#!/usr/bin/env python3
"""
Web服务 - 微服务架构
提供Web界面和API网关功能
"""

import os
import sys
import json
import time
import platform
import requests
import shutil
from datetime import datetime, timedelta
from croniter import croniter
from werkzeug.utils import secure_filename

# 设置时区（仅在Unix/Linux系统上可用）
if platform.system() != 'Windows':
    time.tzset()

# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash
# 设置默认密码哈希方法为pbkdf2:sha256，避免使用scrypt（某些系统不支持）
from functools import partial, wraps
generate_password_hash = partial(generate_password_hash, method='pbkdf2:sha256')
from models import db, User, ScheduledTask, CustomTask, DingTalkRobot, CustomScript, Script, ScriptVersion, TaskExecutionLog, CustomTaskLog, PackageInstallationLog, BackgroundInstallationTask, InstalledPackage, TaskRobotAssociation
from logging.handlers import RotatingFileHandler
from flask_caching import Cache
from system_scripts.resource_monitor import ResourceMonitor
from system_scripts.alert_manager import AlertManager
from system_scripts.monitor_api import app as monitor_api
from system_scripts.user_parameters_manager import parameters_manager
import logging
import psutil

# 配置缓存
cache = Cache(config={
    'CACHE_TYPE': 'simple',
    'CACHE_DEFAULT_TIMEOUT': 60,  # 缓存默认过期时间改为60秒
    'CACHE_THRESHOLD': 1000  # 最大缓存条目数
})

# 配置日志
if not os.path.exists('logs'):
    os.mkdir('logs')
# 检查是否通过start_services.py启动（会有标准输出重定向到日志文件）
is_started_by_service_manager = sys.stdout.name != '<stdout>'
if not is_started_by_service_manager:
    log_date = datetime.now().strftime('%Y-%m-%d')
    file_handler = RotatingFileHandler(f'logs/web_service_{log_date}.log', maxBytes=10240, backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)
else:
    # 如果是通过服务管理器启动，则不创建额外的日志文件
    file_handler = logging.StreamHandler()
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)

app = Flask(__name__, 
           template_folder=os.path.join(os.path.dirname(__file__), '..', 'templates'),
           static_folder=os.path.join(os.path.dirname(__file__), '..', 'static'))

# 注册监控API蓝图
app.register_blueprint(monitor_api, url_prefix='/api/monitor')

# 初始化资源监控和告警管理器
resource_monitor = ResourceMonitor()
alert_manager = AlertManager()
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'web_service_secret_key'

# 数据库配置 - 使用外部MySQL
from config.mysql_config import MySQLConfig
mysql_config = MySQLConfig()
mysql_config.validate_config()

app.config['SQLALCHEMY_DATABASE_URI'] = mysql_config.get_database_uri()
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = mysql_config.get_engine_options()
app.logger.info(f"使用MySQL数据库: {mysql_config.MYSQL_HOST}:{mysql_config.MYSQL_PORT}/{mysql_config.MYSQL_DATABASE}")

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 服务配置
SCHEDULER_SERVICE_URL = os.environ.get('SCHEDULER_SERVICE_URL', 'http://localhost:5001')
EXECUTOR_SERVICE_URL = os.environ.get('EXECUTOR_SERVICE_URL', 'http://localhost:5002')
MONITOR_SERVICE_URL = os.environ.get('MONITOR_SERVICE_URL', 'http://localhost:5003')

# 定义一个通用的函数来处理调度器重新加载
def reload_scheduler(wait=True, timeout=20):
    """重新加载调度器
    
    Args:
        wait (bool): 是否等待当前任务完成，默认为True
        timeout (int): 请求超时时间，默认为20秒
    
    Returns:
        bool: 重新加载是否成功
    """
    try:
        # 增加超时时间，确保有足够时间完成重载
        reload_response = requests.post(
            f"{SCHEDULER_SERVICE_URL}/api/reload", 
            json={'wait': wait}, 
            timeout=timeout
        )
        if reload_response.status_code != 200:
            app.logger.error(f"调度器重新加载失败: {reload_response.text}")
            return False
        return True
    except requests.exceptions.Timeout:
        app.logger.warning("调度器重新加载超时，但这可能是因为重载过程较长，不一定表示失败")
        return False
    except Exception as reload_error:
        app.logger.error(f"调度器重新加载出错: {str(reload_error)}")
        return False

# 文件路径配置
CUSTOM_SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), '..', 'custom_scripts')
if not os.path.exists(CUSTOM_SCRIPTS_DIR):
    os.makedirs(CUSTOM_SCRIPTS_DIR)

# 初始化扩展
db.init_app(app)
cache.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

# 配置日志
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
console_handler.setLevel(logging.INFO)

app.logger.addHandler(file_handler)
app.logger.addHandler(console_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Web服务启动')

# 设置host和port
app.config['HOST'] = '0.0.0.0'
app.config['PORT'] = int(os.environ.get('WEB_SERVICE_PORT', 8088))

@app.route('/health')
def health_check():
    """健康检查端点"""
    try:
        # 检查数据库连接
        from sqlalchemy import text
        db.session.execute(text('SELECT 1'))
        db.session.commit()
        
        # 检查缓存服务
        cache.set('health_check', 'ok')
        cache.get('health_check')
        
        # 检查其他服务连接
        services_status = {
            'scheduler': False,
            'executor': False,
            'monitor': False
        }
        
        try:
            scheduler_resp = requests.get(f"{SCHEDULER_SERVICE_URL}/status", timeout=5)
            services_status['scheduler'] = scheduler_resp.status_code == 200
        except:
            pass
            
        try:
            executor_resp = requests.get(f"{EXECUTOR_SERVICE_URL}/status", timeout=5)
            services_status['executor'] = executor_resp.status_code == 200
        except:
            pass
            
        try:
            monitor_resp = requests.get(f"{MONITOR_SERVICE_URL}/api/monitor/status", timeout=5)
            services_status['monitor'] = monitor_resp.status_code == 200
        except:
            pass
        
        return jsonify({
            'status': 'healthy',
            'timestamp': datetime.now().isoformat(),
            'database': 'connected',
            'cache': 'connected',
            'services': services_status
        })
    except Exception as e:
        app.logger.error(f"健康检查失败: {str(e)}")
        return jsonify({
            'status': 'unhealthy',
            'timestamp': datetime.now().isoformat(),
            'error': str(e)
        }), 500

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route('/')
def index():
    """根路由 - 重定向到登录页面"""
    if current_user.is_authenticated:
        return redirect(url_for('system_monitor_page'))
    return redirect(url_for('login'))

@app.route('/dashboard')
@login_required
def dashboard():  # 移除仪表板缓存，确保实时数据
    """钉钉消息仪表板 - 只显示钉钉消息任务执行数据"""
    # 添加用户ID和管理员标志参数
    user_id = current_user.id
    is_admin = current_user.is_administrator()
    dashboard_type = 'default'  # 普通仪表板，只显示钉钉消息数据
    
    try:
        # 传递用户ID和管理员标志
        scheduler_status = requests.get(
            f"{SCHEDULER_SERVICE_URL}/status", 
            params={'user_id': user_id, 'is_admin': is_admin},
            timeout=10
        ).json()
    except:
        scheduler_status = {'error': '无法连接到调度器服务'}
    
    try:
        executor_status = requests.get(f"{EXECUTOR_SERVICE_URL}/status", timeout=10).json()
    except:
        executor_status = {'error': '无法连接到执行器服务'}

    try:
        monitor_status = requests.get(f"{MONITOR_SERVICE_URL}/api/monitor/status", timeout=10).json()
    except:
        monitor_status = {'error': '无法连接到监控服务'}

    # 统计数据 - 根据用户权限获取，并指定仪表板类型
    stats = get_dashboard_stats(user_id, is_admin)
    logs_stats = get_logs_stats(user_id, is_admin, dashboard_type)
    task_stats = get_task_stats(user_id, is_admin)
    execution_type_stats = get_execution_type_stats(user_id, is_admin, dashboard_type)
    
    # 获取最近执行任务 - 只显示钉钉消息日志
    if is_admin:
        # 管理员可以看到所有任务的执行记录
        recent_executions = TaskExecutionLog.query.filter(
            TaskExecutionLog.log_type == 'dingtalk_log'
        ).order_by(TaskExecutionLog.start_time.desc()).limit(10).all()
    else:
        # 普通用户只能看到自己的任务执行记录
        recent_executions = TaskExecutionLog.query.join(TaskExecutionLog.task).filter(
            ScheduledTask.user_id == user_id,
            TaskExecutionLog.log_type == 'dingtalk_log'
        ).order_by(TaskExecutionLog.start_time.desc()).limit(10).all()

    # 不需要单独提取total_executions，因为它已经包含在logs_stats中
    
    return render_template('dashboard.html', 
                         scheduler_status=scheduler_status,
                         executor_status=executor_status,
                         monitor_status=monitor_status,
                         recent_executions=recent_executions,
                         **stats,
                         **logs_stats,
                         **task_stats,
                         **execution_type_stats)

@cache.memoize(60)
def get_execution_type_stats(user_id=None, is_admin=False, dashboard_type='default'):
    """获取执行类型统计数据
    
    Args:
        user_id: 用户ID
        is_admin: 是否为管理员
        dashboard_type: 仪表板类型，'default'表示普通仪表板(只显示钉钉消息)，'extended'表示扩展仪表板(只显示脚本执行)
    """
    # 初始化所有统计数据
    script_success = 0
    script_failed = 0
    dingtalk_success = 0
    dingtalk_failed = 0
    
    # 根据仪表板类型确定日志类型过滤条件
    log_type_filter = 'dingtalk_log' if dashboard_type == 'default' else 'script_execution'
    
    # 根据用户权限过滤数据
    if is_admin:
        # 管理员可以看到所有日志
        if dashboard_type == 'extended':
            # 脚本执行成功数量
            script_success = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'script_execution',
                TaskExecutionLog.status == '成功'
            ).scalar() or 0
            
            # 脚本执行失败数量
            script_failed = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'script_execution',
                TaskExecutionLog.status == '失败'
            ).scalar() or 0
        else:  # dashboard_type == 'default'
            # 钉钉消息发送成功数量
            dingtalk_success = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'dingtalk_log',
                TaskExecutionLog.status == '成功'
            ).scalar() or 0
            
            # 钉钉消息发送失败数量
            dingtalk_failed = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'dingtalk_log',
                TaskExecutionLog.status == '失败'
            ).scalar() or 0
    else:
        # 普通用户只能看到自己的日志
        # 先获取用户的任务ID列表
        user_task_ids = [task.id for task in ScheduledTask.query.filter_by(user_id=user_id).all()]
        
        if dashboard_type == 'extended':
            # 脚本执行成功数量
            script_success = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'script_execution',
                TaskExecutionLog.status == '成功',
                TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False
            ).scalar() or 0
            
            # 脚本执行失败数量
            script_failed = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'script_execution',
                TaskExecutionLog.status == '失败',
                TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False
            ).scalar() or 0
        else:  # dashboard_type == 'default'
            # 钉钉消息发送成功数量
            dingtalk_success = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'dingtalk_log',
                TaskExecutionLog.status == '成功',
                TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False
            ).scalar() or 0
            
            # 钉钉消息发送失败数量
            dingtalk_failed = db.session.query(db.func.count()).filter(
                TaskExecutionLog.log_type == 'dingtalk_log',
                TaskExecutionLog.status == '失败',
                TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False
            ).scalar() or 0
    
    # 根据仪表板类型返回对应的统计数据
    if dashboard_type == 'extended':
        return {
            'script_success': script_success,
            'script_failed': script_failed,
            # 确保钉钉消息统计数据为0，避免模板中使用这些变量时出错
            'dingtalk_success': 0,
            'dingtalk_failed': 0
        }
    else:  # dashboard_type == 'default'
        return {
            # 确保脚本执行统计数据为0，避免模板中使用这些变量时出错
            'script_success': 0,
            'script_failed': 0,
            'dingtalk_success': dingtalk_success,
            'dingtalk_failed': dingtalk_failed
        }

@cache.memoize(60)
def get_dashboard_stats(user_id=None, is_admin=False):
    """获取基础统计数据"""
    # 根据用户权限过滤数据
    if is_admin:
        # 管理员可以看到所有数据
        task_count = ScheduledTask.query.count()
        active_task_count = ScheduledTask.query.filter_by(is_active=True).count()
        robot_count = DingTalkRobot.query.count()
        # 只统计与钉钉消息任务相关的脚本，不包括自定义任务的脚本
        script_count = db.session.query(db.func.count(db.distinct(ScheduledTask.script_path))).filter(
            ScheduledTask.script_path.isnot(None),
            ScheduledTask.script_path != ''
        ).scalar() or 0
    else:
        # 普通用户只能看到自己的数据
        task_count = ScheduledTask.query.filter_by(user_id=user_id).count()
        active_task_count = ScheduledTask.query.filter_by(user_id=user_id, is_active=True).count()
        robot_count = DingTalkRobot.query.filter_by(user_id=user_id).count()
        # 只统计与钉钉消息任务相关的脚本，不包括自定义任务的脚本
        script_count = db.session.query(db.func.count(db.distinct(ScheduledTask.script_path))).filter(
            ScheduledTask.user_id == user_id,
            ScheduledTask.script_path.isnot(None),
            ScheduledTask.script_path != ''
        ).scalar() or 0
    
    return {
        'task_count': task_count,
        'active_task_count': active_task_count,
        'robot_count': robot_count,
        'script_count': script_count
    }

@cache.memoize(60)
def get_logs_stats(user_id=None, is_admin=False, dashboard_type='default'):
    """获取日志统计数据
    
    Args:
        user_id: 用户ID
        is_admin: 是否为管理员
        dashboard_type: 仪表板类型，'default'表示普通仪表板(只显示钉钉消息)，'extended'表示扩展仪表板(只显示脚本执行)
    """
    now = datetime.now()
    seven_days_ago = now - timedelta(days=7)
    one_day_ago = now - timedelta(days=1)
    
    # 根据仪表板类型确定日志类型过滤条件
    log_type_filter = TaskExecutionLog.log_type == 'dingtalk_log' if dashboard_type == 'default' else TaskExecutionLog.log_type == 'script_execution'

    # 根据用户权限过滤数据
    if is_admin:
        # 管理员可以看到所有日志
        # 使用SQL聚合查询优化统计
        logs_7d = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            TaskExecutionLog.start_time >= seven_days_ago,
            log_type_filter
        ).first()

        logs_24h = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            TaskExecutionLog.start_time >= one_day_ago,
            log_type_filter
        ).first()
        
        # 计算总执行次数
        total_executions = db.session.query(db.func.count()).filter(
            log_type_filter
        ).scalar() or 0
    else:
        # 普通用户只能看到自己的日志
        # 先获取用户的任务ID列表
        user_task_ids = [task.id for task in ScheduledTask.query.filter_by(user_id=user_id).all()]
        
        # 使用SQL聚合查询优化统计，并过滤用户的任务
        logs_7d = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            TaskExecutionLog.start_time >= seven_days_ago,
            TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False,
            log_type_filter
        ).first()

        logs_24h = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            TaskExecutionLog.start_time >= one_day_ago,
            TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False,
            log_type_filter
        ).first()
        
        # 计算总执行次数
        total_executions = db.session.query(db.func.count()).filter(
            TaskExecutionLog.task_id.in_(user_task_ids) if user_task_ids else False,
            log_type_filter
        ).scalar() or 0

    # 处理查询结果，避免None值
    # 确保logs_7d和logs_24h不为None
    if logs_7d is None:
        total_7d = 0
        success_count = 0
    else:
        total_7d = logs_7d.total or 0
        success_count = logs_7d.success or 0
    
    failed_count = total_7d - success_count
    success_rate = int(success_count / total_7d * 100) if total_7d > 0 else 0

    if logs_24h is None:
        total_24h = 0
        success_24h = 0
    else:
        total_24h = logs_24h.total or 0
        success_24h = logs_24h.success or 0
    
    failed_24h = total_24h - success_24h
    success_rate_24h = int(success_24h / total_24h * 100) if total_24h > 0 else 0

    return {
        'success_count': success_count,
        'failed_count': failed_count,
        'success_rate': success_rate,
        'success_24h': success_24h,
        'failed_24h': failed_24h,
        'total_executions': total_executions,
        'success_rate_24h': success_rate_24h,
        'total_24h': total_24h
    }

@cache.memoize(60)
def get_task_stats(user_id=None, is_admin=False):
    """获取任务统计数据"""
    now = datetime.now()
    one_day_ago = now - timedelta(days=1)

    # 根据用户权限过滤数据
    if is_admin:
        # 管理员可以看到所有任务的统计数据
        # 使用SQL聚合查询优化统计
        task_stats = db.session.query(
            ScheduledTask.name,
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).join(TaskExecutionLog.task)\
        .filter(
            TaskExecutionLog.start_time >= one_day_ago,
            TaskExecutionLog.log_type == 'dingtalk_log'  # 只统计钉钉消息日志
        )\
        .group_by(ScheduledTask.name).all()
    else:
        # 普通用户只能看到自己的任务的统计数据
        # 使用SQL聚合查询优化统计，并过滤用户的任务
        task_stats = db.session.query(
            ScheduledTask.name,
            db.func.count().label('total'),
            db.func.sum(db.case((TaskExecutionLog.status == '成功', 1), else_=0)).label('success')
        ).join(TaskExecutionLog.task)\
        .filter(
            TaskExecutionLog.start_time >= one_day_ago,
            ScheduledTask.user_id == user_id,
            TaskExecutionLog.log_type == 'dingtalk_log'  # 只统计钉钉消息日志
        )\
        .group_by(ScheduledTask.name).all()

    task_stats_24h = {}
    for name, total, success in task_stats:
        task_name = name or '未知任务'
        failed = total - (success or 0)
        task_stats_24h[task_name] = {
            'total': total,
            'success': success or 0,
            'failed': failed,
            'success_rate': int((success or 0) / total * 100) if total > 0 else 0
        }

    return {'task_stats_24h': task_stats_24h}

@cache.memoize(60)
def get_custom_task_stats(user_id=None, is_admin=False):
    """获取自定义任务统计数据"""
    now = datetime.now()
    seven_days_ago = now - timedelta(days=7)
    one_day_ago = now - timedelta(days=1)
    
    # 根据用户权限过滤数据
    if is_admin:
        # 管理员可以看到所有自定义任务的统计数据
        # 获取自定义任务总数和活跃任务数
        custom_task_count = CustomTask.query.count()
        active_custom_task_count = CustomTask.query.filter_by(is_active=True).count()
        
        # 使用SQL聚合查询优化统计
        logs_7d = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            CustomTaskLog.start_time >= seven_days_ago
        ).first()

        logs_24h = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            CustomTaskLog.start_time >= one_day_ago
        ).first()
        
        # 计算总执行次数
        total_executions = db.session.query(db.func.count()).scalar() or 0
        
        # 获取每个自定义任务的统计数据，包含用户信息和最近执行时间
        custom_task_stats = db.session.query(
            CustomTask.name,
            User.username,
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success'),
            db.func.max(CustomTaskLog.start_time).label('last_execution_time')
        ).join(CustomTaskLog.task)\
        .join(User, CustomTask.user_id == User.id)\
        .filter(CustomTaskLog.start_time >= one_day_ago)\
        .group_by(CustomTask.name, User.username).all()
    else:
        # 普通用户只能看到自己的自定义任务的统计数据
        # 获取自定义任务总数和活跃任务数
        custom_task_count = CustomTask.query.filter_by(user_id=user_id).count()
        active_custom_task_count = CustomTask.query.filter_by(user_id=user_id, is_active=True).count()
        
        # 先获取用户的自定义任务ID列表
        user_custom_task_ids = [task.id for task in CustomTask.query.filter_by(user_id=user_id).all()]
        
        # 使用SQL聚合查询优化统计，并过滤用户的任务
        logs_7d = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            CustomTaskLog.start_time >= seven_days_ago,
            CustomTaskLog.task_id.in_(user_custom_task_ids) if user_custom_task_ids else False
        ).first()

        logs_24h = db.session.query(
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success')
        ).filter(
            CustomTaskLog.start_time >= one_day_ago,
            CustomTaskLog.task_id.in_(user_custom_task_ids) if user_custom_task_ids else False
        ).first()
        
        # 计算总执行次数
        total_executions = db.session.query(db.func.count()).filter(
            CustomTaskLog.task_id.in_(user_custom_task_ids) if user_custom_task_ids else False
        ).scalar() or 0
        
        # 获取每个自定义任务的统计数据，包含用户信息和最近执行时间
        custom_task_stats = db.session.query(
            CustomTask.name,
            User.username,
            db.func.count().label('total'),
            db.func.sum(db.case((CustomTaskLog.status == '成功', 1), else_=0)).label('success'),
            db.func.max(CustomTaskLog.start_time).label('last_execution_time')
        ).join(CustomTaskLog.task)\
        .join(User, CustomTask.user_id == User.id)\
        .filter(
            CustomTaskLog.start_time >= one_day_ago,
            CustomTask.user_id == user_id
        )\
        .group_by(CustomTask.name, User.username).all()
    
    # 处理查询结果，避免None值
    # 确保logs_7d和logs_24h不为None
    if logs_7d is None:
        total_7d = 0
        success_count = 0
    else:
        total_7d = logs_7d.total or 0
        success_count = logs_7d.success or 0
    
    failed_count = total_7d - success_count
    success_rate = int(success_count / total_7d * 100) if total_7d > 0 else 0

    if logs_24h is None:
        total_24h = 0
        success_24h = 0
    else:
        total_24h = logs_24h.total or 0
        success_24h = logs_24h.success or 0
    
    failed_24h = total_24h - success_24h
    success_rate_24h = int(success_24h / total_24h * 100) if total_24h > 0 else 0
    
    # 处理每个自定义任务的统计数据
    custom_task_stats_24h = {}
    for name, username, total, success, last_execution_time in custom_task_stats:
        task_name = name or '未知任务'
        failed = total - (success or 0)
        # 格式化最近执行时间
        formatted_time = last_execution_time.strftime('%m-%d %H:%M:%S') if last_execution_time else '--'
        custom_task_stats_24h[task_name] = {
            'total': total,
            'success': success or 0,
            'failed': failed,
            'success_rate': int((success or 0) / total * 100) if total > 0 else 0,
            'username': username or '未知用户',
            'last_execution_time': formatted_time
        }
    
    return {
        'custom_task_count': custom_task_count,
        'active_custom_task_count': active_custom_task_count,
        'inactive_custom_task_count': custom_task_count - active_custom_task_count,
        'custom_success_count': success_count,
        'custom_failed_count': failed_count,
        'custom_success_rate': success_rate,
        'custom_success_24h': success_24h,
        'custom_failed_24h': failed_24h,
        'custom_total_executions': total_executions,
        'custom_success_rate_24h': success_rate_24h,
        'custom_total_24h': total_24h,
        'custom_task_stats_24h': custom_task_stats_24h
    }

# 使用缓存获取用户信息
@cache.memoize(300)  # 缓存5分钟
def get_user_by_username(username):
    return User.query.filter_by(username=username).first()

@app.route('/login', methods=['GET', 'POST'])
def login():
    """用户登录"""
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        
        user = get_user_by_username(username)
        if user and check_password_hash(user.password, password):
            login_user(user)
            return redirect(url_for('system_monitor_page'))
        else:
            flash('用户名或密码错误')
    
    return render_template('login.html')

@login_manager.user_loader
@cache.memoize(300)  # 缓存5分钟
def load_user(user_id):
    """从缓存加载用户"""
    return User.query.get(int(user_id))

@app.route('/logout')
@login_required
def logout():
    # 清除所有缓存
    if current_user.is_authenticated:
        cache.delete_memoized(get_user_by_username, current_user.username)
        cache.delete_memoized(load_user, current_user.id)
        cache.delete_memoized(get_dashboard_stats)
        cache.delete_memoized(get_logs_stats)
        cache.delete_memoized(get_task_stats)
        cache.delete_memoized(get_custom_task_stats)
        cache.delete_memoized(get_execution_type_stats)
        cache.clear()
        session.clear()
        logout_user()
    return redirect(url_for('login'))

# 任务管理API
@app.route('/api/tasks', methods=['GET'])
@login_required
@cache.cached(timeout=30)  # 缓存30秒
def get_tasks():
    """获取任务列表"""
    try:
        response = requests.get(f"{SCHEDULER_SERVICE_URL}/api/tasks", timeout=15)
        return jsonify(response.json())
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks', methods=['POST'])
@login_required
def create_task():
    """创建任务"""
    try:
        task_data = request.json
        response = requests.post(f"{SCHEDULER_SERVICE_URL}/api/tasks", 
                               json=task_data, timeout=15)
        
        if response.status_code == 200:
            # 触发调度器重新加载
            # 使用通用函数重新加载调度器
            reload_scheduler(wait=True, timeout=20)
            # 清除相关缓存
            cache.delete_memoized(get_dashboard_stats)
            cache.delete_memoized(get_logs_stats)
            cache.delete_memoized(get_task_stats)
            cache.delete('view//dashboard')
            cache.delete('view//api/tasks')
            cache.delete('view//api/task_statuses')
        
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
@login_required
def update_task(task_id):
    """更新任务"""
    try:
        task_data = request.json
        response = requests.put(f"{SCHEDULER_SERVICE_URL}/api/tasks/{task_id}", 
                              json=task_data, timeout=15)
        
        if response.status_code == 200:
            # 触发调度器重新加载
            reload_scheduler(wait=True, timeout=20)
            # 清除相关缓存
            cache.delete_memoized(get_dashboard_stats)
            cache.delete_memoized(get_logs_stats)
            cache.delete_memoized(get_task_stats)
            cache.delete('view//dashboard')
            cache.delete('view//api/tasks')
            cache.delete('view//api/task_statuses')
            cache.delete_memoized(get_task_logs, task_id)
        
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>', methods=['DELETE'])
@login_required
def delete_task(task_id):
    """删除任务"""
    try:
        response = requests.delete(f"{SCHEDULER_SERVICE_URL}/api/tasks/{task_id}", timeout=15)
        
        if response.status_code == 200:
            # 触发调度器重新加载
            # 使用通用函数重新加载调度器
            reload_scheduler(wait=True, timeout=20)
            # 清除相关缓存
            cache.delete_memoized(get_dashboard_stats)
            cache.delete_memoized(get_logs_stats)
            cache.delete_memoized(get_task_stats)
            cache.delete('view//dashboard')
            cache.delete('view//api/tasks')
            cache.delete('view//api/task_statuses')
            cache.delete_memoized(get_task_logs, task_id)
        
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>/toggle', methods=['POST'])
@login_required
def toggle_task(task_id):
    """切换任务状态"""
    try:
        response = requests.post(f"{SCHEDULER_SERVICE_URL}/api/tasks/{task_id}/toggle", timeout=10)
        
        if response.status_code == 200:
            # 触发调度器重新加载
            reload_scheduler(wait=True, timeout=20)
        
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>/run', methods=['POST'])
@login_required
def run_task(task_id):
    """手动执行任务"""
    try:
        # 先查找任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            return jsonify({'status': 'error', 'message': '任务不存在'}), 404
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            return jsonify({'status': 'error', 'message': '没有权限执行此任务'}), 403
        
        # 调用executor service
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/tasks/{task_id}/run", timeout=30)
        
        if response.status_code == 200:
            # 如果executor service返回成功，统一响应格式
            return jsonify({'status': 'success', 'message': '任务已开始执行'}), 200
        else:
            # 如果executor service返回错误
            try:
                error_data = response.json()
                return jsonify({'status': 'error', 'message': error_data.get('message', '执行失败')}), response.status_code
            except:
                return jsonify({'status': 'error', 'message': '执行失败'}), response.status_code
                
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@app.route('/api/tasks/<int:task_id>/stop', methods=['POST'])
@login_required
def stop_task(task_id):
    """停止任务"""
    try:
        # 先查找任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            return jsonify({'error': '任务不存在'}), 404
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            return jsonify({'error': '没有权限停止此任务'}), 403
        
        # 更新任务状态
        task.status = 'pending'
        db.session.commit()
        
        return jsonify({'status': 'success', 'message': '任务已停止'}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/logs/<int:task_id>')
@login_required
@cache.memoize(30)  # 缓存30秒
def get_task_logs(task_id):
    """获取任务日志"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/logs/{task_id}", timeout=15)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# 添加更多API路由
@app.route('/api/task_statuses')
@login_required
@cache.cached(timeout=15)  # 缓存15秒
def get_task_statuses():
    """获取任务状态"""
    try:
        app.logger.info(f"[DEBUG] 开始获取任务状态")
        response = requests.get(f"{SCHEDULER_SERVICE_URL}/api/tasks", timeout=15)
        tasks_data = response.json()
        app.logger.info(f"[DEBUG] 获取到任务状态数据: {tasks_data}")
        return jsonify(tasks_data)
    except Exception as e:
        app.logger.error(f"[ERROR] 获取任务状态失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/delete_logs', methods=['POST'])
@login_required
def delete_logs():
    """删除日志"""
    try:
        # 检查是否是批量删除特定日志ID
        log_ids = request.json.get('log_ids')
        if log_ids:
            app.logger.info(f"批量删除日志: {log_ids}")
            with app.app_context():
                # 分别处理钉钉任务日志和自定义任务日志
                for log_id in log_ids:
                    # 先尝试删除钉钉任务日志
                    dingtalk_log = TaskExecutionLog.query.get(log_id)
                    if dingtalk_log:
                        db.session.delete(dingtalk_log)
                    else:
                        # 再尝试删除自定义任务日志
                        custom_log = CustomTaskLog.query.get(log_id)
                        if custom_log:
                            db.session.delete(custom_log)
                db.session.commit()
            return jsonify({'success': True, 'message': f'成功删除{len(log_ids)}条日志'})
        else:
            # 原有的按类型删除逻辑
            response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/delete_logs", 
                                json=request.json, timeout=10)
            return jsonify(response.json()), response.status_code
    except Exception as e:
        app.logger.error(f"删除日志失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/create_custom_folder', methods=['POST'])
@login_required
def create_custom_folder_api():
    """创建自定义文件夹 - API版本"""
    try:
        # 处理JSON数据或表单数据
        if request.is_json:
            data = request.json or {}
        else:
            # 处理表单数据
            data = {
                'folder_name': request.form.get('folder_name', '').strip()
            }
        
        # 添加用户信息到请求数据
        data['user_id'] = current_user.id
        
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/create_custom_folder", 
                               json=data, timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/create_custom_folder', methods=['POST'])
@login_required
def create_custom_folder_form():
    """创建自定义文件夹 - 表单版本（保持向后兼容）"""
    try:
        folder_name = request.form.get('folder_name', '').strip()
        
        if not folder_name:
            flash('文件夹名称不能为空', 'error')
            return redirect(url_for('extended_scripts'))
        
        # 验证文件夹名称 - 支持中文
        if len(folder_name) > 50:
            flash('文件夹名称长度不能超过50个字符', 'error')
            return redirect(url_for('extended_scripts'))
        
        # 创建用户目录
        user_dir = os.path.join(CUSTOM_SCRIPTS_DIR, current_user.username)
        if not os.path.exists(user_dir):
            os.makedirs(user_dir)
        
        # 检查文件夹是否已存在
        folder_path = os.path.join(user_dir, folder_name)
        if os.path.exists(folder_path):
            flash('文件夹已存在', 'error')
            return redirect(url_for('extended_scripts'))
        
        # 创建文件夹
        os.makedirs(folder_path)
        flash('文件夹创建成功', 'success')
        
        # 处理重定向参数
        redirect_to = request.form.get('redirect_to')
        folder_path_param = request.form.get('folder_path')
        
        if redirect_to == 'view_custom_folder' and folder_path_param:
            return redirect(url_for('view_custom_folder', folder_name=folder_path_param))
        else:
            return redirect(url_for('extended_scripts'))
            
    except Exception as e:
        flash(f'创建文件夹失败: {str(e)}', 'error')
        return redirect(url_for('extended_scripts'))

# 添加更多API路由
@app.route('/api/list_all_custom_folders')
@login_required
def list_all_custom_folders():
    """获取所有自定义文件夹（管理员）"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/list_all_custom_folders", timeout=5)
        if response.status_code == 200:
            resp = jsonify(response.json())
            # 添加禁止缓存的响应头
            resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
            resp.headers['Pragma'] = 'no-cache'
            resp.headers['Expires'] = '0'
            return resp, response.status_code
        else:
            # 如果API不存在，返回空列表
            resp = jsonify({'folders': []})
            # 添加禁止缓存的响应头
            resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
            resp.headers['Pragma'] = 'no-cache'
            resp.headers['Expires'] = '0'
            return resp, 200
    except Exception as e:
        # 如果连接失败，返回空列表
        resp = jsonify({'folders': []})
        # 添加禁止缓存的响应头
        resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
        resp.headers['Pragma'] = 'no-cache'
        resp.headers['Expires'] = '0'
        return resp, 200

@app.route('/api/list_custom_folders')
@login_required
def list_custom_folders():
    """获取用户自定义文件夹"""
    try:
        # 传递用户信息到executor_service
        headers = {
            'X-User-ID': str(current_user.id),
            'X-Username': current_user.username
        }
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/list_custom_folders", 
                              headers=headers, timeout=5)
        if response.status_code == 200:
            resp = jsonify(response.json())
            # 添加禁止缓存的响应头
            resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
            resp.headers['Pragma'] = 'no-cache'
            resp.headers['Expires'] = '0'
            return resp, response.status_code
        else:
            # 如果API不存在，返回空列表
            resp = jsonify({'folders': []})
            # 添加禁止缓存的响应头
            resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
            resp.headers['Pragma'] = 'no-cache'
            resp.headers['Expires'] = '0'
            return resp, 200
    except Exception as e:
        # 如果连接失败，返回空列表
        resp = jsonify({'folders': []})
        # 添加禁止缓存的响应头
        resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
        resp.headers['Pragma'] = 'no-cache'
        resp.headers['Expires'] = '0'
        return resp, 200

# 添加更多API路由
@app.route('/api/run_custom_task/<int:task_id>', methods=['POST'])
@login_required
def run_custom_task(task_id):
    """运行自定义任务"""
    try:
        app.logger.info(f"尝试运行自定义任务: {task_id}, 调用URL: {EXECUTOR_SERVICE_URL}/api/run_custom_task/{task_id}")
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/run_custom_task/{task_id}", timeout=30)
        app.logger.info(f"运行自定义任务响应: 状态码={response.status_code}, 内容={response.text}")
        return jsonify(response.json()), response.status_code
    except Exception as e:
        app.logger.error(f"运行自定义任务失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/toggle_custom_task/<int:task_id>', methods=['POST'])
@login_required
def toggle_custom_task(task_id):
    """切换自定义任务状态"""
    try:
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/toggle_custom_task/{task_id}", timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/delete_custom_task/<int:task_id>', methods=['DELETE'])
@login_required
def delete_custom_task(task_id):
    """删除自定义任务"""
    try:
        response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_custom_task/{task_id}", timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/delete_robot/<int:robot_id>', methods=['DELETE'])
@login_required
def delete_robot(robot_id):
    """删除机器人"""
    try:
        response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_robot/{robot_id}", timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/delete_script/<int:script_id>', methods=['DELETE', 'GET'])
@login_required
def delete_script(script_id):
    """删除脚本"""
    try:
        # 如果是GET请求，转换为DELETE请求发送给执行器服务
        if request.method == 'GET':
            response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_script/{script_id}", timeout=10)
        else:
            response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_script/{script_id}", timeout=10)
        
        if response.status_code == 200:
            # 使用flash消息通知用户删除成功
            flash('脚本删除成功', 'success')
            # 重定向回脚本管理页面
            return redirect(url_for('script_management'))
        else:
            # API调用失败时，显示错误消息
            flash(f'删除脚本失败: {response.json().get("message", "未知错误")}', 'danger')
            return redirect(url_for('script_management'))
    except Exception as e:
        flash(f'删除脚本失败: {str(e)}', 'danger')
        return redirect(url_for('script_management'))

@app.route('/api/delete_custom_script/<int:script_id>', methods=['DELETE', 'GET', 'POST'])
@login_required
def delete_custom_script(script_id):
    """删除自定义脚本"""
    try:
        if request.method == 'GET':
            # GET方法用于浏览器测试，返回确认页面或直接删除
            response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_custom_script/{script_id}", timeout=10)
            if response.status_code == 200:
                # 使用flash消息通知用户删除成功
                flash('脚本删除成功', 'success')
                # 重定向回扩展脚本页面
                return redirect(url_for('extended_scripts'))
            else:
                flash(f'删除脚本失败: {response.text}', 'danger')
                return redirect(url_for('extended_scripts'))
        else:
            # DELETE方法用于API调用
            response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_custom_script/{script_id}", timeout=10)
            if response.status_code == 200:
                # 如果是API调用，检查是否有指定的重定向参数
                if request.is_json and request.json.get('redirect') == False:
                    # API调用且不需要重定向时，返回JSON响应
                    return jsonify({'success': True, 'message': '脚本删除成功'})
                else:
                    # 使用flash消息通知用户删除成功
                    flash('脚本删除成功', 'success')
                    # 重定向回扩展脚本页面
                    return redirect(url_for('extended_scripts'))
            else:
                if request.is_json and request.json.get('redirect') == False:
                    # API调用且不需要重定向时，返回原始JSON响应
                    return jsonify(response.json()), response.status_code
                else:
                    flash(f'删除脚本失败: {response.json().get("message", "未知错误")}', 'danger')
                    return redirect(url_for('extended_scripts'))
    except Exception as e:
        if request.is_json and request.json.get('redirect') == False:
            return jsonify({'error': str(e)}), 500
        else:
            flash(f'删除脚本失败: {str(e)}', 'danger')
            return redirect(url_for('extended_scripts'))

@app.route('/api/delete_user/<int:user_id>', methods=['DELETE', 'GET'])
@login_required
def delete_user(user_id):
    """删除用户"""
    try:
        # 检查权限
        if not current_user.is_administrator():
            if request.method == 'GET':
                flash('只有管理员可以删除用户', 'error')
                return redirect(url_for('users'))
            return jsonify({'error': '只有管理员可以删除用户'}), 403

        # 检查是否删除自己
        if user_id == current_user.id:
            if request.method == 'GET':
                flash('不能删除当前登录用户', 'error')
                return redirect(url_for('users'))
            return jsonify({'error': '不能删除当前登录用户'}), 400

        # 检查是否删除admin用户
        user = User.query.get(user_id)
        if not user:
            if request.method == 'GET':
                flash('用户不存在', 'error')
                return redirect(url_for('users'))
            return jsonify({'error': '用户不存在'}), 404
            
        if user.username == 'admin':
            if request.method == 'GET':
                flash('不能删除admin用户', 'error')
                return redirect(url_for('users'))
            return jsonify({'error': '不能删除admin用户'}), 400
        
        # 检查用户是否有依赖项
        if user.has_dependencies():
            if request.method == 'GET':
                # 如果是GET请求，重定向到依赖项检查页面
                return redirect(url_for('check_user_dependencies', user_id=user_id))
            else:
                # 如果是API请求，返回错误信息
                return jsonify({
                    'error': '用户有依赖项，无法直接删除',
                    'has_dependencies': True,
                    'check_dependencies_url': url_for('check_user_dependencies', user_id=user_id, _external=True)
                }), 409  # 409 Conflict

        # 发送删除请求到执行器服务
        response = requests.delete(f"{EXECUTOR_SERVICE_URL}/api/delete_user/{user_id}", timeout=10)
        
        if request.method == 'GET':
            if response.status_code == 200:
                flash('用户删除成功', 'success')
            else:
                flash('删除用户失败', 'error')
            return redirect(url_for('users'))
        else:
            return jsonify(response.json()), response.status_code
    except Exception as e:
        if request.method == 'GET':
            flash(f'删除用户失败: {str(e)}', 'error')
            return redirect(url_for('users'))
        return jsonify({'error': str(e)}), 500

@app.route('/test_robot', methods=['POST'])
@login_required
def test_robot():
    """测试钉钉机器人发送"""
    try:
        data = request.get_json()
        webhook = data.get('webhook', '').strip()
        secret = data.get('secret', '').strip()
        
        if not webhook:
            return jsonify({'success': False, 'message': 'Webhook地址不能为空'}), 400
        
        if not secret:
            return jsonify({'success': False, 'message': '加签密钥不能为空'}), 400
        
        # 调用executor service测试机器人
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/test_robot", 
                               json={'webhook': webhook, 'secret': secret}, 
                               timeout=30)
        
        if response.status_code == 200:
            result = response.json()
            return jsonify(result)
        else:
            return jsonify({'success': False, 'message': '测试失败，请检查网络连接'}), 500
            
    except requests.exceptions.Timeout:
        return jsonify({'success': False, 'message': '测试超时，请稍后重试'}), 500
    except requests.exceptions.ConnectionError:
        return jsonify({'success': False, 'message': '无法连接到执行服务，请检查服务状态'}), 500
    except Exception as e:
        return jsonify({'success': False, 'message': f'测试失败: {str(e)}'}), 500

@app.route('/api/download_script/<int:script_id>')
@login_required
def download_script(script_id):
    """下载脚本"""
    try:
        # 直接将请求转发到执行器服务，并将响应原样返回
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/download_script/{script_id}", stream=True, timeout=10)
        
        # 检查响应状态码
        if response.status_code != 200:
            # 如果不是200，可能是错误信息，尝试解析为JSON
            try:
                return jsonify(response.json()), response.status_code
            except:
                return jsonify({'error': '下载脚本失败'}), response.status_code
        
        # 从响应头获取文件名
        filename = response.headers.get('Content-Disposition', '').split('filename=')[-1].strip('"')
        if not filename:
            filename = f"script_{script_id}.py"
        
        # 返回文件内容
        from flask import Response
        return Response(
            response.iter_content(chunk_size=1024),
            content_type=response.headers.get('Content-Type', 'application/octet-stream'),
            headers={
                'Content-Disposition': f'attachment; filename="{filename}"'
            }
        )
    except Exception as e:
        app.logger.error(f"下载脚本失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/transfer_user_dependencies/<int:user_id>', methods=['POST'])
@login_required
def api_transfer_user_dependencies(user_id):
    """转移用户依赖 (API)"""
    try:
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/transfer_user_dependencies/{user_id}", 
                               json=request.json, timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/transfer_user_dependencies/<int:user_id>', methods=['POST'])
@login_required
def transfer_user_dependencies(user_id):
    """处理用户依赖转移表单提交"""
    try:
        # 检查权限
        if not current_user.is_administrator():
            flash('只有管理员可以转移用户依赖项')
            return redirect(url_for('user_management'))
        
        action = request.form.get('action')
        
        # 转移依赖项
        if action == 'transfer':
            heir_id = request.form.get('heir_id')
            if not heir_id:
                flash('请选择继承人')
                return redirect(url_for('check_user_dependencies', user_id=user_id))
            
            # 调用executor服务的API
            response = requests.post(
                f"{EXECUTOR_SERVICE_URL}/api/transfer_user_dependencies/{user_id}",
                json={'target_user_id': heir_id},
                timeout=10
            )
            
            if response.status_code == 200 and response.json().get('success'):
                flash('依赖项转移成功，正在删除用户...')
                # 转移成功后删除用户
                delete_response = requests.delete(
                    f"{EXECUTOR_SERVICE_URL}/api/delete_user/{user_id}",
                    timeout=10
                )
                if delete_response.status_code == 200:
                    flash('用户删除成功')
                else:
                    flash(f'用户删除失败: {delete_response.json().get("error", "未知错误")}')
            else:
                flash(f'依赖项转移失败: {response.json().get("error", "未知错误")}')
        
        # 直接删除用户及其所有数据
        elif action == 'delete_without_transfer':
            # 调用executor服务的API直接删除用户
            delete_response = requests.delete(
                f"{EXECUTOR_SERVICE_URL}/api/delete_user/{user_id}?force=true",
                timeout=10
            )
            if delete_response.status_code == 200:
                flash('用户及其所有数据已成功删除')
            else:
                flash(f'用户删除失败: {delete_response.json().get("error", "未知错误")}')
        
        return redirect(url_for('user_management'))
    except Exception as e:
        flash(f'操作失败: {str(e)}')
        return redirect(url_for('check_user_dependencies', user_id=user_id))

# 添加包管理相关的API路由
@app.route('/api/set_mirror', methods=['POST'])
@login_required
def set_mirror():
    """设置镜像源"""
    try:
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/set_mirror", 
                               json=request.json, timeout=10)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/install_package', methods=['POST'])
@login_required
def install_package():
    """安装包"""
    try:
        # 获取请求数据并添加用户ID
        data = request.json.copy() if request.json else {}
        data['user_id'] = current_user.id
        
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/install_package", 
                               json=data, timeout=30)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/upgrade_package', methods=['POST'])
@login_required
def upgrade_package():
    """升级包"""
    try:
        # 获取请求数据并添加用户ID
        data = request.json.copy() if request.json else {}
        data['user_id'] = current_user.id
        
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/upgrade_package", 
                               json=data, timeout=30)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/uninstall_package', methods=['POST'])
@login_required
def uninstall_package():
    """卸载包"""
    try:
        # 获取请求数据并添加用户ID
        data = request.json.copy() if request.json else {}
        data['user_id'] = current_user.id
        
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/uninstall_package", 
                               json=data, timeout=30)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/refresh_packages', methods=['POST'])
@login_required
def refresh_packages():
    """刷新包列表"""
    try:
        # 导入包管理器
        from system_scripts.package_manager import PackageManager, PROTECTED_PACKAGES, CORE_PACKAGE_DESCRIPTIONS
        
        # 初始化包管理器
        package_manager = PackageManager()
        
        # 获取已安装的包列表
        installed_packages = package_manager.get_installed_packages()
        
        # 标记核心包
        for package in installed_packages:
            package_name = package['name'].lower()
            package['is_core'] = package_name in [p.lower() for p in PROTECTED_PACKAGES]
            package['core_description'] = CORE_PACKAGE_DESCRIPTIONS.get(package_name, '')
        
        # 计算统计数据
        total_packages = len(installed_packages)
        core_packages_count = sum(1 for p in installed_packages if p['is_core'])
        user_packages_count = total_packages - core_packages_count
        
        # 返回包列表和统计数据
        return jsonify({
            'success': True, 
            'message': '包列表已刷新',
            'installed_packages': installed_packages,
            'total_packages': total_packages,
            'user_packages_count': user_packages_count,
            'core_packages_count': core_packages_count
        })
    except Exception as e:
        app.logger.error(f"刷新包列表失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/get_mirrors', methods=['GET'])
@login_required
def get_mirrors():
    """获取镜像源列表"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/get_mirrors", timeout=5)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/get_progress')
@login_required
def get_progress():
    """获取进度"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/get_progress", timeout=5)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/clear_progress', methods=['POST'])
@login_required
def clear_progress():
    """清除进度"""
    try:
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/clear_progress", timeout=5)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/move_to_background', methods=['POST'])
@login_required
def move_to_background():
    """移动到后台"""
    try:
        # 获取请求数据并添加用户ID
        data = request.json.copy() if request.json else {}
        data['user_id'] = current_user.id
        
        # 转发请求到执行器服务
        response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/move_to_background", 
                               json=data, timeout=5)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        app.logger.error(f"移动任务到后台失败: {str(e)}")
        return jsonify({'success': False, 'message': f"移动任务到后台失败: {str(e)}"}), 500

@app.route('/api/check_running_tasks')
@login_required
def check_running_tasks():
    """检查运行中的任务"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/check_running_tasks", timeout=5)
        return jsonify(response.json()), response.status_code
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# 添加文件管理相关的API路由
@app.route('/api/upload_files', methods=['POST'])
@login_required
def upload_files():
    """上传文件"""
    try:
        # 获取请求参数
        path = request.form.get('path', '')
        target_user_id = request.form.get('user_id', current_user.id)
        
        # 权限检查
        if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
            return jsonify({'success': False, 'message': '无权限上传文件到其他用户目录'}), 403
        
        # 获取目标用户
        target_user = User.query.get(target_user_id)
        if not target_user:
            return jsonify({'success': False, 'message': '用户不存在'}), 404
        
        # 构建用户文件目录路径
        user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
        if not os.path.exists(user_files_dir):
            os.makedirs(user_files_dir)
        
        # 构建当前路径
        current_path = path.strip('/') if path else ''
        upload_dir = os.path.join(user_files_dir, current_path) if current_path else user_files_dir
        
        # 安全检查：确保路径在用户目录内
        if not os.path.normpath(upload_dir).startswith(os.path.normpath(user_files_dir)):
            return jsonify({'success': False, 'message': '无效的路径'}), 400
        
        # 确保上传目录存在
        if not os.path.exists(upload_dir):
            os.makedirs(upload_dir)
        
        # 处理上传的文件
        if 'files' not in request.files:
            return jsonify({'success': False, 'message': '没有文件被上传'}), 400
        
        files = request.files.getlist('files')
        if not files or files[0].filename == '':
            return jsonify({'success': False, 'message': '没有选择文件'}), 400
        
        # 保存所有文件
        saved_files = []
        for file in files:
            filename = file.filename
            file_path = os.path.join(upload_dir, filename)
            
            # 检查文件是否已存在
            if os.path.exists(file_path):
                return jsonify({'success': False, 'message': f'文件 {filename} 已存在'}), 400
            
            # 保存文件
            file.save(file_path)
            saved_files.append(filename)
        
        return jsonify({'success': True, 'message': f'成功上传 {len(saved_files)} 个文件', 'files': saved_files})
    except Exception as e:
        app.logger.error(f"文件上传失败: {str(e)}")
        return jsonify({'success': False, 'message': f'上传失败: {str(e)}'}), 500

@app.route('/api/delete_file', methods=['POST'])
@login_required
def delete_file():
    """删除文件"""
    try:
        # 获取请求参数
        data = request.json
        file_path = data.get('path')
        
        if not file_path:
            return jsonify({'success': False, 'message': '缺少文件路径'}), 400
        
        # 获取当前用户
        target_user_id = request.args.get('user_id', current_user.id)
        
        # 权限检查
        if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
            return jsonify({'success': False, 'message': '无权限删除其他用户的文件'}), 403
        
        # 获取目标用户
        target_user = User.query.get(target_user_id) or current_user
        
        # 构建用户文件目录路径
        user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
        
        try:
            # 安全处理文件路径
            sanitized_path = file_path.lstrip('/')
            # 检查路径中是否包含可疑的序列
            if '..' in sanitized_path or '\\' in sanitized_path:
                app.logger.warning(f"view_file接口检测到可疑的文件路径: {sanitized_path}")
                return jsonify({'success': False, 'message': '无效的路径'}), 400
                
            full_path = os.path.join(user_files_dir, sanitized_path)
            
            # 安全检查：确保路径在用户目录内
            if not os.path.normpath(full_path).startswith(os.path.normpath(user_files_dir)):
                app.logger.warning(f"view_file接口检测到路径越界: {full_path}")
                return jsonify({'success': False, 'message': '无效的路径'}), 400
        except Exception as e:
            app.logger.error(f"view_file接口处理文件路径时出错: {str(e)}")
            return jsonify({'success': False, 'message': '处理文件路径时出错'}), 400
        
        # 检查文件是否存在
        if not os.path.exists(full_path):
            return jsonify({'success': False, 'message': '文件不存在'}), 404
        
        # 检查文件是否已注册为脚本
        if os.path.isfile(full_path):
            custom_script = CustomScript.query.filter_by(file_path=full_path).first()
            if custom_script:
                return jsonify({'success': False, 'message': '此文件已注册为脚本，请先删除脚本'}), 400
        
        # 检查文件夹内是否有已注册的脚本
        if os.path.isdir(full_path):
            has_protected_scripts = False
            protected_script_name = None
            
            for root, dirs, files in os.walk(full_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    custom_script = CustomScript.query.filter_by(file_path=file_path).first()
                    if custom_script:
                        has_protected_scripts = True
                        protected_script_name = file
                        app.logger.warning(f"检测到受保护的脚本: {file_path}, 脚本ID: {custom_script.id}, 脚本名称: {custom_script.name}")
                        break
                if has_protected_scripts:
                    break
            
            if has_protected_scripts:
                app.logger.warning(f"尝试删除包含已注册脚本的文件夹被阻止: {full_path}, 脚本: {protected_script_name}")
                return jsonify({'success': False, 'message': f'文件夹内包含已注册的脚本 {protected_script_name}，请先删除脚本'}), 400
        
        # 删除文件或文件夹
        if os.path.isfile(full_path):
            os.remove(full_path)
        else:
            shutil.rmtree(full_path)
        
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        app.logger.error(f"文件删除失败: {str(e)}")
        return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}), 500

@app.route('/api/download_file')
@login_required
def download_file():
    """下载文件"""
    try:
        # 获取请求参数
        file_path = request.args.get('path')
        if not file_path:
            return jsonify({'success': False, 'message': '缺少文件路径'}), 400
            
        # 确保file_path是字符串类型
        if not isinstance(file_path, str):
            app.logger.warning(f"view_file接口file_path参数类型异常: {type(file_path)}")
            try:
                file_path = str(file_path)
            except Exception as e:
                app.logger.error(f"view_file接口file_path参数转换为字符串失败: {str(e)}")
                return jsonify({'success': False, 'message': '文件路径格式错误'}), 400
        
        # 获取当前用户
        target_user_id = request.args.get('user_id', current_user.id)
        
        # 权限检查
        if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
            return jsonify({'success': False, 'message': '无权限下载其他用户的文件'}), 403
        
        # 获取目标用户
        target_user = User.query.get(target_user_id) or current_user
        
        # 构建用户文件目录路径
        user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
        
        try:
            # 安全处理文件路径
            sanitized_path = file_path.lstrip('/')
            # 检查路径中是否包含可疑的序列
            if '..' in sanitized_path or '\\' in sanitized_path:
                app.logger.warning(f"download_file接口检测到可疑的文件路径: {sanitized_path}")
                return jsonify({'success': False, 'message': '无效的路径'}), 400
                
            full_path = os.path.join(user_files_dir, sanitized_path)
            
            # 安全检查：确保路径在用户目录内
            if not os.path.normpath(full_path).startswith(os.path.normpath(user_files_dir)):
                app.logger.warning(f"download_file接口检测到路径越界: {full_path}")
                return jsonify({'success': False, 'message': '无效的路径'}), 400
        except Exception as e:
            app.logger.error(f"download_file接口处理文件路径时出错: {str(e)}")
            return jsonify({'success': False, 'message': '处理文件路径时出错'}), 400
        
        # 检查文件是否存在
        if not os.path.exists(full_path) or not os.path.isfile(full_path):
            return jsonify({'success': False, 'message': '文件不存在或不是文件'}), 404
        
        # 获取文件名
        filename = os.path.basename(full_path)
        
        try:
            # 检查文件大小
            file_size = os.path.getsize(full_path)
            app.logger.info(f"下载文件: {full_path}, 大小: {file_size} 字节")
            
            # 检查文件是否可读
            if not os.access(full_path, os.R_OK):
                app.logger.warning(f"download_file接口文件无读取权限: {full_path}")
                return jsonify({'success': False, 'message': '无权限读取文件'}), 403
                
            # 发送文件
            return send_file(full_path, as_attachment=True, download_name=filename)
        except PermissionError:
            app.logger.warning(f"download_file接口无权限读取文件: {full_path}")
            return jsonify({'success': False, 'message': '无权限读取文件'}), 403
        except FileNotFoundError:
            app.logger.warning(f"download_file接口文件不存在: {full_path}")
            return jsonify({'success': False, 'message': '文件不存在'}), 404
    except Exception as e:
        app.logger.error(f"文件下载失败: {str(e)}")
        return jsonify({'success': False, 'message': f'下载失败: {str(e)}'}), 500

@app.route('/api/view_file')
@login_required
def view_file():
    """查看文件"""
    try:
        # 获取请求参数
        file_path = request.args.get('path')
        if not file_path:
            return jsonify({'success': False, 'message': '缺少文件路径'}), 400
        
        # 获取当前用户
        target_user_id = request.args.get('user_id', current_user.id)
        
        # 权限检查
        if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
            return jsonify({'success': False, 'message': '无权限查看其他用户的文件'}), 403
        
        # 获取目标用户
        target_user = User.query.get(target_user_id) or current_user
        
        # 构建用户文件目录路径
        user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
        full_path = os.path.join(user_files_dir, file_path.lstrip('/'))
        
        # 安全检查：确保路径在用户目录内
        if not os.path.normpath(full_path).startswith(os.path.normpath(user_files_dir)):
            return jsonify({'success': False, 'message': '无效的路径'}), 400
        
        # 检查文件是否存在
        if not os.path.exists(full_path) or not os.path.isfile(full_path):
            return jsonify({'success': False, 'message': '文件不存在或不是文件'}), 404
        
        # 检查文件大小
        file_size = os.path.getsize(full_path)
        if file_size > 5 * 1024 * 1024:  # 5MB限制
            return jsonify({'success': False, 'message': '文件过大，无法预览（限制5MB）'}), 400
        
        # 尝试以不同编码读取文本文件
        encodings = ['utf-8', 'gbk', 'latin-1', 'cp1252', 'iso-8859-1']
        content = None
        used_encoding = None
        
        for encoding in encodings:
            try:
                with open(full_path, 'r', encoding=encoding) as f:
                    content = f.read()
                used_encoding = encoding
                break
            except UnicodeDecodeError:
                continue
        
        if content is None:
            app.logger.warning(f"view_file接口无法解码文件: {full_path}")
            return jsonify({'success': False, 'message': '文件编码不支持，无法预览'}), 400
        
        # 获取文件名和扩展名
        filename = os.path.basename(full_path)
        _, ext = os.path.splitext(filename)
        ext = ext.lstrip('.').lower()
        
        # 返回文件内容和元数据
        return jsonify({
            'success': True,
            'content': content,
            'filename': filename,
            'extension': ext,
            'size': file_size,
            'path': file_path
        })
    except PermissionError:
        app.logger.warning(f"view_file接口无权限读取文件: {full_path}")
        return jsonify({'success': False, 'message': '无权限读取文件'}), 403
    except IsADirectoryError:
        app.logger.warning(f"view_file接口尝试读取目录: {full_path}")
        return jsonify({'success': False, 'message': '无法预览目录'}), 400
    except FileNotFoundError:
        app.logger.warning(f"view_file接口文件不存在: {full_path}")
        return jsonify({'success': False, 'message': '文件不存在'}), 404
    except Exception as e:
        app.logger.error(f"查看文件失败: {str(e)}")
        return jsonify({'success': False, 'message': f'查看文件失败: {str(e)}'}), 500

@app.route('/api/list_files')
@login_required
def list_files():
    """列出文件"""
    try:
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/list_files", 
                              params=request.args, timeout=10)
        
        # 验证响应数据格式
        response_data = response.json()
        if response.status_code != 200:
            app.logger.warning(f"调用executor_service的list_files接口失败: {response.status_code}, {response_data}")
            return jsonify(response_data), response.status_code
        
        # 验证返回的数据结构
        if 'files' not in response_data:
            app.logger.warning(f"executor_service的list_files接口返回数据格式异常: {response_data}")
            return jsonify({'error': '服务返回数据格式异常'}), 500
            
        # 确保files是列表类型
        if not isinstance(response_data['files'], list):
            app.logger.warning(f"executor_service的list_files接口返回的files不是列表: {type(response_data['files'])}")
            response_data['files'] = []
            
        return jsonify(response_data), response.status_code
    except ValueError as e:
        app.logger.error(f"解析executor_service的list_files接口响应JSON失败: {str(e)}")
        return jsonify({'error': '解析服务响应失败'}), 500
    except Exception as e:
        app.logger.error(f"调用executor_service的list_files接口异常: {str(e)}")
        return jsonify({'error': str(e)}), 500

# 页面路由
@app.route('/robot_management')
@login_required
def robot_management():
    """机器人管理页面"""
    with app.app_context():
        # 根据用户权限获取机器人
        if current_user.is_administrator():
            # 管理员可以看到所有机器人
            robots = DingTalkRobot.query.all()
        else:
            # 普通用户只能看到自己的机器人
            robots = DingTalkRobot.query.filter_by(user_id=current_user.id).all()
        return render_template('robots.html', robots=robots)

@app.route('/script_management')
@login_required
def script_management():
    """脚本管理页面"""
    with app.app_context():
        # 根据用户权限获取脚本
        if current_user.is_administrator():
            # 管理员可以看到所有脚本
            scripts = Script.query.all()
        else:
            # 普通用户只能看到自己的脚本
            scripts = Script.query.filter_by(user_id=current_user.id).all()
        
        # 为每个脚本添加相对路径属性
        for script in scripts:
            # 如果路径以'scripts/'开头，则直接显示相对路径
            if script.path.startswith('scripts/'):
                script.relative_path = script.path
            else:
                # 提取文件名作为相对路径
                script.relative_path = os.path.basename(script.path)
        
        return render_template('scripts.html', scripts=scripts)

@app.route('/scheduled_tasks')
@login_required
def scheduled_tasks():
    """定时任务页面 - 只显示钉钉机器人任务"""
    with app.app_context():
        db.session.expire_all()  # 强制刷新ORM缓存
        # 根据用户权限获取钉钉任务，不获取自定义任务
        if current_user.is_administrator():
            # 管理员可以看到所有任务
            dingtalk_tasks = ScheduledTask.query.all()
        else:
            # 普通用户只能看到自己的任务
            dingtalk_tasks = ScheduledTask.query.filter_by(user_id=current_user.id).all()
        app.logger.info(f"[DEBUG] 获取到 {len(dingtalk_tasks)} 个钉钉任务")
        # 转换为字典格式
        tasks = []
        for task in dingtalk_tasks:
            # 动态计算下次执行时间
            next_run = None
            if task.is_active and task.cron_expression:
                try:
                    app.logger.info(f"[DEBUG] 计算任务ID={task.id} 的下次执行时间，cron表达式={task.cron_expression}")
                    base = datetime.now()
                    itr = croniter(task.cron_expression, base)
                    next_run = itr.get_next(datetime)
                    app.logger.info(f"[DEBUG] 任务ID={task.id} 计算成功，next_run_time={next_run}")
                except Exception as e:
                    app.logger.error(f"[ERROR] 任务ID={task.id} 计算下次执行时间失败: {str(e)}")
                    # 尝试修复cron表达式并重新计算
                    try:
                        # 确保cron表达式格式正确
                        cron_parts = task.cron_expression.strip().split()
                        if len(cron_parts) == 5:
                            app.logger.info(f"[DEBUG] 尝试修复任务ID={task.id}的cron表达式并重新计算")
                            base = datetime.now()
                            itr = croniter(task.cron_expression, base)
                            next_run = itr.get_next(datetime)
                            app.logger.info(f"[DEBUG] 任务ID={task.id} 修复后计算成功，next_run_time={next_run}")
                    except Exception as e2:
                        app.logger.error(f"[ERROR] 任务ID={task.id} 修复后计算下次执行时间仍然失败: {str(e2)}")
                        next_run = None
            else:
                app.logger.info(f"[DEBUG] 任务ID={task.id} 未激活或无cron表达式，不计算下次执行时间")
            
            app.logger.info(f"[DEBUG] 任务ID={task.id} next_run_time={next_run} 类型={type(next_run)}")
            tasks.append({
                'id': task.id,
                'name': task.name,
                'type': 'dingtalk',
                'cron_expression': task.cron_expression,
                'script_path': task.script_path,
                'is_active': task.is_active,
                'status': task.status,
                'last_run_time': task.last_run_time,
                'next_run_time': next_run,
                'last_run_status': task.last_run_status,
                'user': task.user,
                'robots': task.robots,
                'description': None
            })
        return render_template('scheduled_tasks.html', tasks=tasks, datetime=datetime, croniter=croniter)

@app.route('/system-monitor')
@login_required
def system_monitor():
    """系统资源监控页面"""
    return render_template('system_monitor.html')

@app.route('/extended_dashboard')
@login_required
def extended_dashboard():
    """自定义任务仪表板 - 只显示自定义任务数据"""
    # 复用 dashboard 的统计逻辑
    from datetime import datetime, timedelta
    from models import ScheduledTask, DingTalkRobot, CustomScript, TaskExecutionLog, CustomTask, CustomTaskLog
    
    # 获取当前用户ID和管理员标志
    user_id = current_user.id
    is_admin = current_user.is_administrator()
    dashboard_type = 'extended'  # 扩展仪表板，只显示自定义任务数据
    
    try:
        # 传递用户ID和管理员标志
        scheduler_status = requests.get(
            f"{SCHEDULER_SERVICE_URL}/status", 
            params={'user_id': user_id, 'is_admin': is_admin},
            timeout=5
        ).json()
    except:
        scheduler_status = {'error': '无法连接到调度器服务'}
    try:
        executor_status = requests.get(f"{EXECUTOR_SERVICE_URL}/status", timeout=5).json()
    except:
        executor_status = {'error': '无法连接到执行器服务'}
    
    # 根据用户权限获取统计数据，并指定仪表板类型
    # 不再获取普通任务的统计数据，只获取自定义任务的统计数据
    custom_task_stats = get_custom_task_stats(user_id, is_admin)
    # 保留logs_stats和execution_type_stats，因为模板中可能仍然使用它们的某些字段
    logs_stats = get_logs_stats(user_id, is_admin, dashboard_type)
    execution_type_stats = get_execution_type_stats(user_id, is_admin, dashboard_type)
    
    # 不再从stats和logs_stats中提取数据，直接使用custom_task_stats中的数据
    # 这样可以确保仪表板只显示自定义任务的统计数据
    
    # 不再获取脚本执行日志，只显示自定义任务的执行记录
    recent_executions = []
    
    # 非活跃任务数量已经在custom_task_stats中计算好了
    
    # 获取自定义任务的最近执行记录
    if is_admin:
        # 管理员可以看到所有自定义任务的执行记录
        recent_custom_executions = CustomTaskLog.query.order_by(CustomTaskLog.start_time.desc()).limit(10).all()
    else:
        # 普通用户只能看到自己的自定义任务执行记录
        recent_custom_executions = CustomTaskLog.query.join(CustomTaskLog.task).filter(
            CustomTask.user_id == user_id
        ).order_by(CustomTaskLog.start_time.desc()).limit(10).all()
    
    return render_template('extended_dashboard.html',
        scheduler_status=scheduler_status,
        executor_status=executor_status,
        task_count=custom_task_stats['custom_task_count'],
        active_task_count=custom_task_stats['active_custom_task_count'],
        inactive_task_count=custom_task_stats['inactive_custom_task_count'],
        custom_task_count=custom_task_stats['custom_task_count'],
        active_custom_task_count=custom_task_stats['active_custom_task_count'],
        inactive_custom_task_count=custom_task_stats['inactive_custom_task_count'],
        custom_success_count=custom_task_stats['custom_success_count'],
        custom_failed_count=custom_task_stats['custom_failed_count'],
        custom_success_rate=custom_task_stats['custom_success_rate'],
        custom_total_executions=custom_task_stats['custom_total_executions'],
        custom_success_24h=custom_task_stats['custom_success_24h'],
        custom_failed_24h=custom_task_stats['custom_failed_24h'],
        custom_success_rate_24h=custom_task_stats['custom_success_rate_24h'],
        custom_total_24h=custom_task_stats['custom_total_24h'],
        custom_task_stats=custom_task_stats.get('custom_task_stats_24h', {}),
        recent_custom_executions=recent_custom_executions,
        **logs_stats,
        **execution_type_stats)

@app.route('/extended_tasks')
@login_required
def extended_tasks():
    """扩展任务页面"""
    # 处理任务执行后的 URL 参数，显示 flash 消息
    task_executed = request.args.get('task_executed')
    status = request.args.get('status')
    message = request.args.get('message')
    
    if task_executed and status:
        if status == 'success':
            flash('任务已开始执行', 'success')
        elif status == 'error' and message:
            flash(f'执行失败: {message}', 'danger')
    
    # 如果是管理员，显示所有任务；否则只显示当前用户的任务
    if current_user.is_administrator():
        tasks = CustomTask.query.order_by(CustomTask.created_at.desc()).all()
    else:
        tasks = CustomTask.query.filter_by(user_id=current_user.id).order_by(CustomTask.created_at.desc()).all()
    
    # 动态计算每个任务的下次执行时间
    for task in tasks:
        # 动态计算下次执行时间
        next_run = None
        if task.is_active and task.cron_expression:
            try:
                app.logger.info(f"[DEBUG] 计算任务ID={task.id} 的下次执行时间，cron表达式={task.cron_expression}")
                base = datetime.now()
                itr = croniter(task.cron_expression, base)
                next_run = itr.get_next(datetime)
                app.logger.info(f"[DEBUG] 任务ID={task.id} 计算成功，next_run_time={next_run}")
            except Exception as e:
                app.logger.error(f"[ERROR] 任务ID={task.id} 计算下次执行时间失败: {str(e)}")
                # 尝试修复cron表达式并重新计算
                try:
                    # 确保cron表达式格式正确
                    cron_parts = task.cron_expression.strip().split()
                    if len(cron_parts) == 5:
                        app.logger.info(f"[DEBUG] 尝试修复任务ID={task.id}的cron表达式并重新计算")
                        base = datetime.now()
                        itr = croniter(task.cron_expression, base)
                        next_run = itr.get_next(datetime)
                        app.logger.info(f"[DEBUG] 任务ID={task.id} 修复后计算成功，next_run_time={next_run}")
                except Exception as e2:
                    app.logger.error(f"[ERROR] 任务ID={task.id} 修复后计算下次执行时间仍然失败: {str(e2)}")
                    next_run = None
        else:
            app.logger.info(f"[DEBUG] 任务ID={task.id} 未激活或无cron表达式，不计算下次执行时间")
        
        app.logger.info(f"[DEBUG] 任务ID={task.id} next_run_time={next_run} 类型={type(next_run)}")
        task.next_run_time = next_run
    
    return render_template('extended_tasks.html', tasks=tasks, datetime=datetime, croniter=croniter)

@app.route('/extended_scripts')
@login_required
def extended_scripts():
    """扩展脚本页面"""
    # 如果是管理员，显示所有脚本；否则只显示当前用户的脚本
    if current_user.is_administrator():
        scripts = CustomScript.query.order_by(CustomScript.upload_time.desc()).all()
    else:
        scripts = CustomScript.query.filter_by(user_id=current_user.id).order_by(CustomScript.upload_time.desc()).all()
    
    # 不再直接从文件系统获取文件夹列表
    # 文件夹列表将通过前端JavaScript调用API获取
    # 这样可以确保页面初始加载和后续刷新显示的数据一致
    return render_template('extended_scripts.html', scripts=scripts)

@app.route('/delete_custom_folder/<folder_name>', methods=['GET', 'POST'])
@login_required
def delete_custom_folder(folder_name):
    """删除自定义文件夹"""
    try:
        # 检查是否是管理员删除其他用户的文件夹
        user_id = request.args.get('user_id')
        if user_id and current_user.username == 'admin':
            # 管理员删除其他用户的文件夹
            target_user_id = user_id
        else:
            # 普通用户删除自己的文件夹
            target_user_id = current_user.id
        
        app.logger.info(f"用户 {current_user.username} 尝试删除文件夹: {folder_name}, 目标用户ID: {target_user_id}")
        
        # 调用executor_service的API删除文件夹
        response = requests.delete(
            f"{EXECUTOR_SERVICE_URL}/api/delete_custom_folder/{folder_name}",
            params={'user_id': target_user_id},
            timeout=10
        )
        
        result = response.json()
        if response.status_code == 200 and result.get('success'):
            app.logger.info(f"文件夹删除成功: {folder_name}")
            flash('文件夹删除成功', 'success')
        else:
            error_msg = result.get('error', '未知错误')
            app.logger.warning(f"删除文件夹失败: {error_msg}, 状态码: {response.status_code}")
            flash(f'文件夹删除失败: {error_msg}', 'danger')
            
    except requests.exceptions.ConnectionError:
        app.logger.error(f"删除文件夹 {folder_name} 失败: 无法连接到执行服务")
        flash('无法连接到执行服务，请检查服务状态', 'danger')
    except requests.exceptions.Timeout:
        app.logger.error(f"删除文件夹 {folder_name} 失败: 操作超时")
        flash('删除操作超时，请稍后重试', 'danger')
    except Exception as e:
        app.logger.error(f"删除文件夹 {folder_name} 失败: {str(e)}")
        flash(f'文件夹删除失败: {str(e)}', 'danger')
    
    return redirect(url_for('extended_scripts'))



@app.route('/file_management')
@login_required
def file_management():
    """文件管理页面"""
    # 获取请求参数
    path = request.args.get('path', '')
    target_user_id = request.args.get('user_id', current_user.id)
    
    # 管理员可以查看所有用户的文件
    if current_user.is_administrator():
        all_users = User.query.all()
        if target_user_id and target_user_id != current_user.id:
            target_user = User.query.get(target_user_id)
            if not target_user:
                flash('用户不存在', 'danger')
                return redirect(url_for('file_management'))
            current_user_name = target_user.username
        else:
            target_user_id = current_user.id
            current_user_name = current_user.username
    else:
        all_users = [current_user]
        target_user_id = current_user.id
        current_user_name = current_user.username
    
    # 获取目标用户
    target_user = User.query.get(target_user_id)
    if not target_user:
        flash('用户不存在', 'danger')
        return redirect(url_for('file_management'))
    
    # 构建用户文件目录路径
    user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
    if not os.path.exists(user_files_dir):
        os.makedirs(user_files_dir)
    
    # 构建当前路径
    current_path = path.strip('/') if path else ''
    # 如果当前路径是字符串而不是列表，则正常处理
    if isinstance(current_path, str):
        full_path = os.path.join(user_files_dir, current_path) if current_path else user_files_dir
    else:
        # 处理current_path可能是列表的情况
        full_path = user_files_dir
    
    # 安全检查：确保路径在用户目录内
    if not os.path.normpath(full_path).startswith(os.path.normpath(user_files_dir)):
        flash('无效的路径', 'danger')
        return redirect(url_for('file_management', user_id=target_user_id))
    
    # 检查路径是否存在
    if not os.path.exists(full_path) or not os.path.isdir(full_path):
        flash('目录不存在', 'danger')
        return redirect(url_for('file_management', user_id=target_user_id))
    
    # 构建面包屑导航
    try:
        # 确保current_path是字符串类型
        if not isinstance(current_path, str):
            app.logger.warning(f"构建面包屑时current_path类型异常: {type(current_path)}")
            current_path = str(current_path) if current_path is not None else ''
        breadcrumb = current_path.split('/') if current_path else []
    except Exception as e:
        app.logger.error(f"构建面包屑导航时出错: {str(e)}")
        breadcrumb = []
    
    # 获取文件列表
    files = []
    try:
        # 调用list_files API获取文件和目录列表
        response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/list_files", 
                              params={'user_id': target_user_id, 'path': current_path}, 
                              timeout=10)
        
        if response.status_code != 200:
            app.logger.warning(f"调用executor_service的list_files接口失败: {response.status_code}")
            flash('获取文件列表失败', 'danger')
            files = []
        else:
            response_data = response.json()
            if 'files' not in response_data:
                app.logger.warning(f"executor_service的list_files接口返回数据格式异常")
                flash('获取文件列表失败: 数据格式错误', 'danger')
                files = []
            else:
                files_data = response_data['files']
                
                # 处理文件大小格式化
                for file in files_data:
                    size = file.get('size', 0)
                    if not file.get('is_dir', False):
                        if size < 1024:
                            file['size'] = f"{size} B"
                        elif size < 1024 * 1024:
                            file['size'] = f"{size/1024:.1f} KB"
                        elif size < 1024 * 1024 * 1024:
                            file['size'] = f"{size/(1024*1024):.1f} MB"
                        else:
                            file['size'] = f"{size/(1024*1024*1024):.1f} GB"
                    else:
                        file['size'] = ""
                
                files = files_data
    except ValueError as e:
        app.logger.error(f"解析executor_service的list_files接口响应JSON失败: {str(e)}")
        flash('获取文件列表失败: 解析响应失败', 'danger')
        files = []
    except Exception as e:
        app.logger.error(f"获取文件列表失败: {str(e)}")
        flash(f'获取文件列表失败: {str(e)}', 'danger')
        files = []
    
    return render_template('file_management.html',
                         current_user_name=current_user_name,
                         target_user_id=target_user_id,
                         current_path=current_path,
                         all_users=all_users,
                         files=files,
                         breadcrumb=breadcrumb)

@app.route('/package_management')
@login_required
def package_management():
    """包管理页面"""
    try:
        # 导入包管理器
        from system_scripts.package_manager import PackageManager, PROTECTED_PACKAGES, CORE_PACKAGE_DESCRIPTIONS
        
        # 初始化包管理器
        package_manager = PackageManager()
        
        # 获取已安装的包列表
        installed_packages = package_manager.get_installed_packages()
        
        # 标记核心包
        for package in installed_packages:
            package_name = package['name'].lower()
            package['is_core'] = package_name in [p.lower() for p in PROTECTED_PACKAGES]
            package['core_description'] = CORE_PACKAGE_DESCRIPTIONS.get(package_name, '')
        
        # 计算统计数据
        total_packages = len(installed_packages)
        core_packages_count = sum(1 for p in installed_packages if p['is_core'])
        user_packages_count = total_packages - core_packages_count
        
        # 最近操作次数（暂时硬编码）
        recent_operations = 0
        
        return render_template('package_management.html',
                             installed_packages=installed_packages,
                             total_packages=total_packages,
                             user_packages_count=user_packages_count,
                             core_packages_count=core_packages_count,
                             recent_operations=recent_operations)
    except Exception as e:
        app.logger.error(f"加载包管理页面失败: {str(e)}")
        # 出错时使用默认数据
        return render_template('package_management.html',
                             installed_packages=[],
                             total_packages=0,
                             user_packages_count=0,
                             core_packages_count=0,
                             recent_operations=0)

@app.route('/user_management')
@app.route('/users')
@login_required
def user_management():
    """用户管理页面"""
    # 检查权限
    if not current_user.is_administrator():
        flash('只有管理员可以访问用户管理页面', 'error')
        return redirect(url_for('dashboard'))

    with app.app_context():
        # 获取所有用户
        users = User.query.all()
        return render_template('users.html', users=users)

@app.route('/about_project')
@login_required
def about_project():
    """关于项目页面"""
    return render_template('about_project.html')

@app.route('/change_password', methods=['GET', 'POST'])
@login_required
def change_password():
    """修改密码页面"""
    if request.method == 'POST':
        current_password = request.form.get('current_password')
        new_password = request.form.get('new_password')
        confirm_password = request.form.get('confirm_password')
        
        # 验证表单数据
        if not current_password or not new_password or not confirm_password:
            flash('所有字段都必须填写')
            return render_template('change_password.html')
        
        # 验证新密码长度
        if len(new_password) < 6:
            flash('新密码长度不能少于6位')
            return render_template('change_password.html')
        
        # 验证新密码和确认密码是否一致
        if new_password != confirm_password:
            flash('新密码和确认密码不一致')
            return render_template('change_password.html')
        
        # 验证当前密码是否正确
        user = User.query.get(current_user.id)
        if not check_password_hash(user.password, current_password):
            flash('当前密码不正确')
            return render_template('change_password.html')
        
        # 更新密码
        try:
            user.password = generate_password_hash(new_password)
            db.session.commit()
            flash('密码修改成功')
            # 清除用户缓存
            cache.delete_memoized(get_user_by_username, user.username)
            cache.delete_memoized(load_user, user.id)
            return redirect(url_for('dashboard'))
        except Exception as e:
            db.session.rollback()
            flash(f'密码修改失败: {str(e)}')
    
    return render_template('change_password.html')

# 添加更多缺失的路由
@app.route('/edit_robot/<int:robot_id>', methods=['GET', 'POST'])
@login_required
def edit_robot(robot_id):
    """编辑机器人页面"""
    with app.app_context():
        robot = DingTalkRobot.query.get(robot_id)
        if not robot:
            flash('机器人不存在')
            return redirect(url_for('robot_management'))
        
        # 检查权限
        if not current_user.is_administrator() and robot.user_id != current_user.id:
            flash('没有权限编辑此机器人')
            return redirect(url_for('robot_management'))
        
        if request.method == 'POST':
            # 处理表单提交
            try:
                robot.name = request.form['name']
                robot.tag = request.form['tag']
                robot.webhook = request.form['webhook']
                robot.secret = request.form['secret']
                
                db.session.commit()
                flash('机器人信息更新成功')
                return redirect(url_for('robot_management'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'更新机器人信息失败: {str(e)}')
        
        return render_template('edit_robot.html', robot=robot)

@app.route('/add_script', methods=['GET', 'POST'])
@login_required
def add_script():
    """添加脚本页面"""
    if request.method == 'POST':
        try:
            # 获取表单数据
            name = request.form.get('name', '').strip()
            content = request.form.get('content', '').strip()
            description = request.form.get('description', '').strip()
            
            # 验证必填字段
            if not name or not content:
                flash('脚本名称和内容不能为空', 'danger')
                return redirect(url_for('add_script'))
            
            # 验证脚本名称格式 - 支持中文
            if not name or len(name) > 50:
                flash('脚本名称不能为空且长度不能超过50个字符', 'danger')
                return redirect(url_for('add_script'))
            
            # 检查脚本名称是否已存在
            if Script.query.filter_by(name=name).first():
                flash('同名脚本已存在', 'danger')
                return redirect(url_for('add_script'))
            
            # 生成文件路径
            filename = f"{name}.py"
            file_path = os.path.join('scripts', filename)
            
            # 确保目录存在
            os.makedirs('scripts', exist_ok=True)
            
            # 创建脚本文件
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            # 创建脚本记录
            script = Script(
                name=name,
                path=file_path,
                description=description if description else None,
                user_id=current_user.id,
                current_version=1
            )
            db.session.add(script)
            
            # 创建初始版本记录
            version = ScriptVersion(
                script_id=script.id,
                version=1,
                content=content,
                commit_message='创建脚本',
                created_by_id=current_user.id
            )
            db.session.add(version)
            
            db.session.commit()
            flash('脚本创建成功', 'success')
            return redirect(url_for('script_management'))
            
        except Exception as e:
            db.session.rollback()
            flash(f'创建脚本失败: {str(e)}', 'danger')
            return redirect(url_for('add_script'))
    
    return render_template('add_script.html')

@app.route('/edit_script/<int:script_id>', methods=['GET', 'POST'])
@login_required
def edit_script(script_id):
    """编辑脚本页面"""
    with app.app_context():
        script = Script.query.get(script_id)
        if not script:
            flash('脚本不存在')
            return redirect(url_for('script_management'))
        
        # 检查权限
        if not current_user.is_administrator() and script.user_id != current_user.id:
            flash('没有权限编辑此脚本')
            return redirect(url_for('script_management'))
        
        if request.method == 'POST':
            # 处理表单提交
            try:
                content = request.form['content']
                commit_message = request.form.get('commit_message', '更新脚本')
                
                # 创建新版本
                current_version = script.current_version or 0
                new_version = current_version + 1
                
                # 创建版本记录
                version = ScriptVersion(
                    script_id=script.id,
                    version=new_version,
                    content=content,
                    commit_message=commit_message,
                    created_by_id=current_user.id
                )
                db.session.add(version)
                
                # 更新脚本当前版本
                script.current_version = new_version
                
                # 写入文件
                with open(script.path, 'w', encoding='utf-8') as f:
                    f.write(content)
                
                db.session.commit()
                flash('脚本更新成功')
                return redirect(url_for('script_management'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'更新脚本失败: {str(e)}')
        
        # 读取脚本内容
        try:
            with open(script.path, 'r', encoding='utf-8') as f:
                script_content = f.read()
        except Exception as e:
            script_content = f"# 无法读取文件: {str(e)}"
        
        # 获取版本历史
        versions = ScriptVersion.query.filter_by(script_id=script.id)\
            .order_by(ScriptVersion.version.desc()).all()
        
        return render_template('edit_script.html', 
                             script=script, 
                             script_content=script_content,
                             versions=versions)

@app.route('/script_version/<int:script_id>/<int:version>')
@login_required
def view_script_version(script_id, version):
    """查看脚本历史版本"""
    with app.app_context():
        script = Script.query.get(script_id)
        if not script:
            flash('脚本不存在')
            return redirect(url_for('script_management'))
        
        # 检查权限
        if not current_user.is_administrator() and script.user_id != current_user.id:
            flash('没有权限查看此脚本')
            return redirect(url_for('script_management'))
        
        # 获取指定版本
        version_record = ScriptVersion.query.filter_by(
            script_id=script_id,
            version=version
        ).first()
        
        if not version_record:
            flash('指定版本不存在')
            return redirect(url_for('edit_script', script_id=script_id))
        
        return render_template('view_script_version.html',
                             script=script,
                             version=version_record)

@app.route('/script_version/<int:script_id>/<int:version>/rollback', methods=['POST'])
@login_required
def rollback_script_version(script_id, version):
    """回滚到指定版本"""
    with app.app_context():
        script = Script.query.get(script_id)
        if not script:
            flash('脚本不存在')
            return redirect(url_for('script_management'))
        
        # 检查权限
        if not current_user.is_administrator() and script.user_id != current_user.id:
            flash('没有权限操作此脚本')
            return redirect(url_for('script_management'))
        
        # 获取指定版本
        version_record = ScriptVersion.query.filter_by(
            script_id=script_id,
            version=version
        ).first()
        
        if not version_record:
            flash('指定版本不存在')
            return redirect(url_for('edit_script', script_id=script_id))
        
        try:
            # 创建新版本（回滚操作）
            new_version = script.current_version + 1
            rollback_version = ScriptVersion(
                script_id=script.id,
                version=new_version,
                content=version_record.content,
                commit_message=f'回滚到版本 {version}',
                created_by_id=current_user.id
            )
            db.session.add(rollback_version)
            
            # 更新脚本当前版本
            script.current_version = new_version
            
            # 写入文件
            with open(script.path, 'w', encoding='utf-8') as f:
                f.write(version_record.content)
            
            db.session.commit()
            flash('脚本已回滚到指定版本')
            
        except Exception as e:
            db.session.rollback()
            flash(f'回滚失败: {str(e)}')
        
        return redirect(url_for('edit_script', script_id=script_id))

@app.route('/upload_script', methods=['GET', 'POST'])
@login_required
def upload_script():
    """上传脚本页面"""
    if request.method == 'POST':
        try:
            # 获取上传的文件
            file = request.files.get('script')
            
            # 验证文件是否存在
            if not file or file.filename == '':
                flash('请选择要上传的脚本文件', 'danger')
                return redirect(url_for('upload_script'))
            
            # 验证文件类型
            if not file.filename.endswith('.py'):
                flash('只支持Python脚本文件(.py)', 'danger')
                return redirect(url_for('upload_script'))
            
            # 获取脚本名称（去掉.py后缀）
            script_name = os.path.splitext(file.filename)[0]
            
            # 验证脚本名称格式 - 支持中文
            import re
            if not script_name or len(script_name) > 50:
                flash('脚本名称不能为空且长度不能超过50个字符', 'danger')
                return redirect(url_for('upload_script'))
            
            # 确保scripts目录存在
            os.makedirs('scripts', exist_ok=True)
            
            # 生成文件路径
            file_path = os.path.join('scripts', file.filename)
            
            # 检查文件是否已存在 - 使用绝对路径检查
            abs_file_path = os.path.abspath(file_path)
            app.logger.info(f'检查文件路径: {abs_file_path}')
            
            # 检查scripts目录是否存在
            scripts_dir = os.path.abspath('scripts')
            app.logger.info(f'Scripts目录: {scripts_dir}, 是否存在: {os.path.exists(scripts_dir)}')
            
            # 列出scripts目录中的文件
            if os.path.exists(scripts_dir):
                files_in_dir = os.listdir(scripts_dir)
                app.logger.info(f'Scripts目录中的文件: {files_in_dir}')
            
            if os.path.exists(abs_file_path):
                app.logger.info(f'文件已存在: {abs_file_path}')
                flash(f'同名脚本文件已存在: {abs_file_path}', 'danger')
                return redirect(url_for('upload_script'))
            else:
                app.logger.info(f'文件不存在: {abs_file_path}')
            
            # 检查数据库中是否已存在同名脚本
            existing_script = Script.query.filter_by(name=script_name).first()
            app.logger.info(f'检查数据库中的脚本: {script_name}, 结果: {existing_script}')
            if existing_script:
                flash(f'数据库中已存在同名脚本: {script_name}（ID: {existing_script.id}）', 'danger')
                return redirect(url_for('upload_script'))
            
            # 保存文件 - 使用绝对路径
            file.save(abs_file_path)
            
            # 读取文件内容
            with open(abs_file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # 创建脚本记录 - 存储绝对路径
            script = Script(
                name=script_name,
                path=abs_file_path,
                user_id=current_user.id,
                current_version=1
            )
            db.session.add(script)
            # 先提交以获取script.id
            db.session.flush()
            
            # 创建初始版本记录
            version = ScriptVersion(
                script_id=script.id,
                version=1,
                content=content,
                commit_message='上传脚本',
                created_by_id=current_user.id  # 注意这里应该是created_by_id而不是created_by
            )
            db.session.add(version)
            
            db.session.commit()
            flash('脚本上传成功', 'success')
            return redirect(url_for('script_management'))
            
        except Exception as e:
            db.session.rollback()
            flash(f'上传脚本失败: {str(e)}', 'danger')
            return redirect(url_for('upload_script'))
    
    return render_template('upload_script.html')

@app.route('/edit_task/<int:task_id>', methods=['GET', 'POST'])
@login_required
def edit_task(task_id):
    """编辑任务页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限编辑此任务')
            return redirect(url_for('scheduled_tasks'))
        
        if request.method == 'POST':
            # 处理表单提交
            try:
                task.name = request.form['name']
                task.cron_expression = request.form['cron_expression']
                
                # 处理重试配置字段
                if 'max_retries' in request.form and request.form['max_retries']:
                    task.max_retries = int(request.form['max_retries'])
                if 'retry_delay' in request.form and request.form['retry_delay']:
                    task.retry_delay = int(request.form['retry_delay'])
                
                if isinstance(task, ScheduledTask):
                    task.script_path = request.form['script_path']
                    # 只保存前端填写的params
                    params_str = request.form['params']
                    task.params = params_str
                    # 处理机器人关联
                    robot_ids = request.form.get('robot_ids', '').split(',')
                    robot_ids = [int(rid) for rid in robot_ids if rid.strip()]
                    # 处理机器人@人配置
                    robot_at_configs = {}
                    for key, value in request.form.items():
                        if key.startswith('robot_at_') and value.strip():
                            robot_id = key.replace('robot_at_', '')
                            robot_at_configs[robot_id] = value.strip()
                    # 设置机器人和@人配置（通过多对多表）
                    task.set_robots(robot_ids, robot_at_configs)
                
                db.session.commit()
                
                # 触发调度器重新加载
                reload_scheduler(wait=True, timeout=20)
                
                flash('任务更新成功')
                return redirect(url_for('scheduled_tasks'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'更新任务失败: {str(e)}')
        
        # 根据用户权限获取脚本列表
        if current_user.is_administrator():
            # 管理员可以看到所有脚本
            scripts = Script.query.all()
        else:
            # 普通用户只能看到自己的脚本
            scripts = Script.query.filter_by(user_id=current_user.id).all()
        
        # 根据用户权限获取机器人列表
        if current_user.is_administrator():
            # 管理员可以看到所有机器人
            robots = DingTalkRobot.query.all()
        else:
            # 普通用户只能看到自己的机器人
            robots = DingTalkRobot.query.filter_by(user_id=current_user.id).all()
        
        # 获取当前任务的机器人@人配置
        robot_at_configs = {}
        if isinstance(task, ScheduledTask):
            robot_at_configs = task.get_robot_at_configs()
        
        return render_template('edit_task.html', task=task, scripts=scripts, robots=robots, robot_at_configs=robot_at_configs)

@app.route('/edit_user/<int:user_id>', methods=['GET', 'POST'])
@login_required
def edit_user(user_id):
    """编辑用户页面"""
    with app.app_context():
        user = User.query.get(user_id)
        if not user:
            flash('用户不存在')
            return redirect(url_for('user_management'))
        
        # 检查权限
        if not current_user.is_administrator():
            flash('没有权限编辑用户')
            return redirect(url_for('user_management'))
        
        if request.method == 'POST':
            # 处理表单提交
            try:
                user.username = request.form['username']
                user.is_admin = 'is_admin' in request.form
                
                # 处理功能权限
                user.can_use_dingtalk = 'can_use_dingtalk' in request.form
                user.can_use_custom_tasks = 'can_use_custom_tasks' in request.form
                user.can_manage_packages = 'can_manage_packages' in request.form
                
                db.session.commit()
                flash('用户信息更新成功')
                return redirect(url_for('user_management'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'更新用户信息失败: {str(e)}')
        
        return render_template('edit_user.html', user=user)

@app.route('/add_custom_task', methods=['GET', 'POST'])
@login_required
def add_custom_task():
    """添加自定义任务页面"""
    with app.app_context():
        if request.method == 'POST':
            try:
                # 获取表单数据
                name = request.form.get('name', '').strip()
                description = request.form.get('description', '').strip()
                script_id = request.form.get('script_id')
                cron_expression = request.form.get('cron_expression', '').strip()
                
                # 验证必填字段
                if not name:
                    flash('任务名称不能为空', 'danger')
                    return redirect(url_for('add_custom_task'))
                
                if not script_id:
                    flash('请选择要执行的脚本', 'danger')
                    return redirect(url_for('add_custom_task'))
                
                if not cron_expression:
                    flash('Cron表达式不能为空', 'danger')
                    return redirect(url_for('add_custom_task'))
                
                # 验证脚本是否存在且属于当前用户
                script = CustomScript.query.get(script_id)
                if not script:
                    flash('选择的脚本不存在', 'danger')
                    return redirect(url_for('add_custom_task'))
                
                if not current_user.is_administrator() and script.user_id != current_user.id:
                    flash('没有权限使用此脚本', 'danger')
                    return redirect(url_for('add_custom_task'))
                
                # 在数据库事务内进行重复名称检查和创建
                try:
                    # 验证任务名称是否已存在（在事务内检查）
                    existing_task = CustomTask.query.filter_by(
                        name=name, 
                        user_id=current_user.id
                    ).first()
                    if existing_task:
                        flash('同名任务已存在', 'danger')
                        return redirect(url_for('add_custom_task'))
                    
                    # 获取重试配置字段
                    max_retries = request.form.get('max_retries', '3')
                    retry_delay = request.form.get('retry_delay', '300')
                    
                    # 创建新任务
                    task = CustomTask(
                        name=name,
                        description=description if description else None,
                        script_id=int(script_id),
                        cron_expression=cron_expression,
                        user_id=current_user.id,
                        is_active=True,
                        max_retries=int(max_retries),
                        retry_delay=int(retry_delay)
                    )
                    db.session.add(task)
                    db.session.commit()
                    
                    # 触发调度器重新加载
                    reload_scheduler(wait=True, timeout=20)  # 即使失败也会继续执行
                    
                    flash('自定义任务创建成功', 'success')
                    return redirect(url_for('extended_tasks'))
                    
                except Exception as db_error:
                    db.session.rollback()
                    # 检查是否是唯一约束错误
                    error_msg = str(db_error).lower()
                    if 'unique' in error_msg or 'duplicate' in error_msg:
                        flash('任务名称已存在，请使用其他名称', 'danger')
                    else:
                        flash(f'创建任务失败: {str(db_error)}', 'danger')
                    return redirect(url_for('add_custom_task'))
                
            except Exception as e:
                flash(f'处理请求失败: {str(e)}', 'danger')
                return redirect(url_for('add_custom_task'))
        
        # GET请求：获取用户的自定义脚本
        if current_user.is_administrator():
            # 管理员可以看到所有脚本
            scripts = CustomScript.query.filter_by(is_active=True).all()
        else:
            # 普通用户只能看到自己的脚本
            scripts = CustomScript.query.filter_by(user_id=current_user.id, is_active=True).all()
        
        return render_template('add_custom_task.html', scripts=scripts)

@app.route('/edit_custom_task/<int:task_id>', methods=['GET', 'POST'])
@login_required
def edit_custom_task(task_id):
    """编辑自定义任务页面"""
    with app.app_context():
        task = CustomTask.query.get(task_id)
        if not task:
            flash('任务不存在')
            return redirect(url_for('extended_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限编辑此任务')
            return redirect(url_for('extended_tasks'))
        
        if request.method == 'POST':
            # 处理表单提交
            try:
                task.name = request.form['name']
                task.description = request.form['description']
                task.script_id = int(request.form['script_id'])
                task.cron_expression = request.form['cron_expression']
                
                # 处理重试配置字段
                if 'max_retries' in request.form and request.form['max_retries']:
                    task.max_retries = int(request.form['max_retries'])
                if 'retry_delay' in request.form and request.form['retry_delay']:
                    task.retry_delay = int(request.form['retry_delay'])
                
                db.session.commit()
                
                # 触发调度器重新加载
                reload_scheduler(wait=True, timeout=20)
                
                flash('自定义任务更新成功')
                return redirect(url_for('extended_tasks'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'更新自定义任务失败: {str(e)}')
        
        # 获取用户的自定义脚本
        scripts = CustomScript.query.filter_by(user_id=current_user.id).all()
        
        return render_template('edit_custom_task.html', task=task, scripts=scripts)

@app.route('/toggle_custom_task/<int:task_id>')
@login_required
def toggle_custom_task_page(task_id):
    """切换自定义任务状态页面路由"""
    try:
        with app.app_context():
            task = CustomTask.query.get(task_id)
            if not task:
                flash('任务不存在', 'danger')
                return redirect(url_for('extended_tasks'))
            
            # 检查权限
            if not current_user.is_administrator() and task.user_id != current_user.id:
                flash('没有权限操作此任务', 'danger')
                return redirect(url_for('extended_tasks'))
            
            # 切换任务状态
            task.is_active = not task.is_active
            db.session.commit()
            
            # 触发调度器重新加载
            reload_scheduler(wait=True, timeout=20)  # 即使失败也会继续执行
            
            action = '启用' if task.is_active else '禁用'
            flash(f'任务已{action}', 'success')
            
    except Exception as e:
        db.session.rollback()
        flash(f'操作失败: {str(e)}', 'danger')
    
    return redirect(url_for('extended_tasks'))

@app.route('/delete_custom_task/<int:task_id>')
@login_required
def delete_custom_task_page(task_id):
    """删除自定义任务页面路由"""
    try:
        with app.app_context():
            task = CustomTask.query.get(task_id)
            if not task:
                flash('任务不存在', 'danger')
                return redirect(url_for('extended_tasks'))
            
            # 检查权限
            if not current_user.is_administrator() and task.user_id != current_user.id:
                flash('没有权限删除此任务', 'danger')
                return redirect(url_for('extended_tasks'))
            
            # 删除任务
            task_name = task.name
            
            # 删除关联的日志
            CustomTaskLog.query.filter_by(task_id=task_id).delete()
            
            db.session.delete(task)
            db.session.commit()
            
            # 触发调度器重新加载
            reload_scheduler(wait=True, timeout=20)  # 即使失败也会继续执行
            
            flash(f'任务 "{task_name}" 已删除', 'success')
            
    except Exception as e:
        db.session.rollback()
        flash(f'删除失败: {str(e)}', 'danger')
    
    return redirect(url_for('extended_tasks'))

@app.route('/upload_custom_script', methods=['GET', 'POST'])
@login_required
def upload_custom_script():
    if request.method == 'POST':
        try:
            # 获取表单数据
            script_name = request.form.get('name', '').strip()
            description = request.form.get('description', '').strip()
            folder = request.form.get('folder', '').strip()
            file = request.files.get('script')
            
            # 验证必填字段
            if not script_name:
                flash('脚本名称不能为空', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            if not file or file.filename == '':
                flash('请选择要上传的脚本文件', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            # 验证文件类型
            if not file.filename.endswith('.py'):
                flash('只支持Python脚本文件(.py)', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            # 验证脚本名称格式 - 支持中文
            import re
            if not script_name or len(script_name) > 50:
                flash('脚本名称不能为空且长度不能超过50个字符', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            # 确定保存路径
            user_dir = os.path.join('custom_scripts', current_user.username)
            if folder:
                # 验证文件夹名称 - 支持中文
                if not folder or len(folder) > 50:
                    flash('文件夹名称不能为空且长度不能超过50个字符', 'danger')
                    return redirect(url_for('upload_custom_script'))
                user_dir = os.path.join(user_dir, folder)
            
            # 确保目录存在
            if not os.path.exists(user_dir):
                os.makedirs(user_dir)
            
            # 生成文件名（使用脚本名称 + .py）
            filename = f"{script_name}.py"
            file_path = os.path.join(user_dir, filename)
            
            # 检查文件是否已存在
            if os.path.exists(file_path):
                flash('同名脚本文件已存在', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            # 检查数据库中是否已存在同名脚本
            existing_script = CustomScript.query.filter_by(
                name=script_name, 
                user_id=current_user.id
            ).first()
            if existing_script:
                flash('同名脚本已存在', 'danger')
                return redirect(url_for('upload_custom_script'))
            
            # 保存文件
            file.save(file_path)
            
            # 创建数据库记录
            script = CustomScript(
                name=script_name,
                filename=filename,
                file_path=file_path,
                file_size=os.path.getsize(file_path),
                description=description if description else None,
                user_id=current_user.id,
                is_active=True
            )
            db.session.add(script)
            db.session.commit()
            
            flash('脚本上传成功', 'success')
            
            # 如果是上传到特定文件夹，重定向到文件夹页面
            if folder:
                return redirect(url_for('view_custom_folder', folder_path=folder))
            else:
                return redirect(url_for('extended_scripts'))
                
        except Exception as e:
            db.session.rollback()
            flash(f'上传脚本失败: {str(e)}', 'danger')
            return redirect(url_for('upload_custom_script'))
    
    return render_template('upload_custom_script.html')

@app.route('/delete_custom_script/<int:script_id>', methods=['POST'])
@login_required
def delete_custom_script_view(script_id):
    script = CustomScript.query.get(script_id)
    if not script:
        flash('脚本不存在', 'danger')
        return redirect(url_for('extended_scripts'))
    if not current_user.is_administrator() and script.user_id != current_user.id:
        flash('没有权限删除此脚本', 'danger')
        return redirect(url_for('extended_scripts'))
    
    # 获取重定向目标
    redirect_to = request.form.get('redirect_to', 'extended_scripts')
    folder_path = request.form.get('folder_path', '')
    
    try:
        if os.path.exists(script.file_path):
            os.remove(script.file_path)
        
        # 删除关联的自定义任务
        CustomTask.query.filter_by(script_id=script_id).delete()
        
        db.session.delete(script)
        db.session.commit()
        flash('脚本删除成功', 'success')
    except Exception as e:
        db.session.rollback()
        flash(f'删除脚本失败: {str(e)}', 'danger')
    
    # 根据来源页面重定向
    if redirect_to == 'view_custom_folder' and folder_path:
        return redirect(url_for('view_custom_folder', folder_path=folder_path))
    else:
        return redirect(url_for('extended_scripts'))

@app.route('/edit_custom_script/<int:script_id>', methods=['GET', 'POST'])
@login_required
def edit_custom_script(script_id):
    script = CustomScript.query.get(script_id)
    if not script:
        if request.method == 'POST':
            return jsonify({'success': False, 'message': '脚本不存在'})
        flash('脚本不存在', 'danger')
        return redirect(url_for('extended_scripts'))
    
    # 检查权限
    if not current_user.is_administrator() and script.user_id != current_user.id:
        if request.method == 'POST':
            return jsonify({'success': False, 'message': '没有权限编辑此脚本'})
        flash('没有权限编辑此脚本', 'danger')
        return redirect(url_for('extended_scripts'))
    
    if request.method == 'POST':
        try:
            # 处理JSON请求
            if request.is_json:
                data = request.get_json()
                content = data.get('content', '')
            else:
                # 处理表单请求
                content = request.form.get('content', '')
            
            if not content:
                return jsonify({'success': False, 'message': '脚本内容不能为空'})
            
            # 保存文件
            with open(script.file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            # 更新文件大小
            script.file_size = os.path.getsize(script.file_path)
            db.session.commit()
            
            if request.is_json:
                return jsonify({'success': True, 'message': '脚本保存成功'})
            else:
                flash('脚本内容已更新', 'success')
                return redirect(url_for('extended_scripts'))
                
        except Exception as e:
            db.session.rollback()
            error_msg = f'保存失败: {str(e)}'
            if request.is_json:
                return jsonify({'success': False, 'message': error_msg})
            else:
                flash(error_msg, 'danger')
                return redirect(url_for('extended_scripts'))
    
    # GET请求：读取脚本内容
    try:
        with open(script.file_path, 'r', encoding='utf-8') as f:
            script_content = f.read()
    except Exception as e:
        script_content = f'# 无法读取文件: {str(e)}'
    
    return render_template('edit_custom_script.html', script=script, script_content=script_content)

@app.route('/view_file')
@login_required
def view_file_page():
    """查看文件页面"""
    try:
        # 获取请求参数
        file_path = request.args.get('path')
        if not file_path:
            flash('缺少文件路径', 'danger')
            return redirect(url_for('file_management'))
        
        # 获取当前用户
        target_user_id = request.args.get('user_id', current_user.id)
        
        # 权限检查
        if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
            flash('无权限查看其他用户的文件', 'danger')
            return redirect(url_for('file_management'))
        
        # 获取目标用户
        target_user = User.query.get(target_user_id) or current_user
        
        # 构建用户文件目录路径
        user_files_dir = os.path.join(CUSTOM_SCRIPTS_DIR, target_user.username)
        full_path = os.path.join(user_files_dir, file_path.lstrip('/'))
        
        # 安全检查：确保路径在用户目录内
        if not os.path.normpath(full_path).startswith(os.path.normpath(user_files_dir)):
            flash('无效的路径', 'danger')
            return redirect(url_for('file_management'))
        
        # 检查文件是否存在
        if not os.path.exists(full_path) or not os.path.isfile(full_path):
            flash('文件不存在或不是文件', 'danger')
            return redirect(url_for('file_management'))
        
        # 检查文件大小
        file_size = os.path.getsize(full_path)
        if file_size > 5 * 1024 * 1024:  # 5MB限制
            flash('文件过大，无法预览（限制5MB）', 'warning')
            return redirect(url_for('file_management'))
        
        # 获取文件信息
        filename = os.path.basename(full_path)
        _, ext = os.path.splitext(filename)
        ext = ext.lstrip('.').lower()
        
        # 获取文件创建和修改时间
        stat = os.stat(full_path)
        created = datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')
        modified = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
        
        # 格式化文件大小
        if file_size < 1024:
            size_str = f"{file_size} B"
        elif file_size < 1024 * 1024:
            size_str = f"{file_size/1024:.1f} KB"
        else:
            size_str = f"{file_size/(1024*1024):.1f} MB"
        
        # 处理Excel文件
        is_excel = ext in ['xlsx', 'xls']
        excel_data = None
        
        if is_excel:
            try:
                import pandas as pd
                # 读取Excel文件，只读取前100行
                df = pd.read_excel(full_path, nrows=100)
                # 转换为HTML表格
                excel_data = df.to_html(classes='table table-striped table-bordered', index=False)
                # 如果实际行数超过100行，添加提示信息
                if len(df) == 100:
                    excel_data += '<div class="notice-content mt-3"><i class="fas fa-info-circle"></i> 注意：为了提高性能，仅显示前100行数据。</div>'
                encoding = 'excel'
                content = ''
            except Exception as e:
                app.logger.error(f"Excel文件处理失败: {str(e)}")
                is_excel = False
                content = f"Excel文件处理失败: {str(e)}"
        else:
            # 尝试以不同编码读取文本文件
            encodings = ['utf-8', 'gbk', 'latin-1', 'cp1252', 'iso-8859-1']
            content = None
            encoding = None
            
            for enc in encodings:
                try:
                    with open(full_path, 'r', encoding=enc) as f:
                        content = f.read()
                    encoding = enc
                    break
                except UnicodeDecodeError:
                    continue
            
            if content is None:
                flash('文件编码不支持，无法预览', 'warning')
                return redirect(url_for('file_management'))
        
        # 获取上级目录路径
        parent_path = os.path.dirname(file_path)
        
        return render_template('view_file.html',
                             filename=filename,
                             content=content,
                             extension=ext,
                             size=size_str,
                             created=created,
                             modified=modified,
                             encoding=encoding,
                             path=file_path,
                             parent_path=parent_path,
                             target_user_id=target_user_id,
                             is_excel=is_excel,
                             excel_data=excel_data)
    except Exception as e:
        app.logger.error(f"查看文件页面失败: {str(e)}")
        flash(f'查看文件失败: {str(e)}', 'danger')
        return redirect(url_for('file_management'))

@app.route('/task_logs')
@login_required
def task_logs():
    """任务日志页面 - 重定向到带任务ID的日志页面"""
    task_id = request.args.get('task_id')
    if task_id:
        return redirect(url_for('task_logs_page', task_id=task_id))
    flash('未指定任务ID')
    return redirect(url_for('scheduled_tasks'))

@app.route('/delete_logs', methods=['POST'])
@login_required
def delete_logs_page():
    """删除日志页面路由"""
    try:
        # 获取要删除的日志ID列表
        log_ids = request.json.get('log_ids', [])
        if not log_ids:
            return jsonify({'success': False, 'message': '未指定要删除的日志'}), 400
            
        app.logger.info(f"用户 {current_user.username} 正在删除日志: {log_ids}")
        
        # 调用API删除日志
        response = requests.post(
            f"{request.url_root.rstrip('/')}/api/delete_logs",
            json={'log_ids': log_ids},
            headers={'Content-Type': 'application/json'},
            timeout=10
        )
        
        if response.status_code == 200 and response.json().get('success'):
            app.logger.info(f"成功删除 {len(log_ids)} 条日志")
            return jsonify({'success': True, 'message': f'成功删除 {len(log_ids)} 条日志'})
        else:
            error_msg = response.json().get('error', '未知错误')
            app.logger.warning(f"删除日志失败: {error_msg}")
            return jsonify({'success': False, 'message': f'删除失败: {error_msg}'}), 400
    except Exception as e:
        app.logger.error(f"删除日志异常: {str(e)}")
        return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}), 500

@app.route('/task_log_detail/<int:log_id>')
@login_required
def task_log_detail(log_id):
    """任务日志详情页面"""
    with app.app_context():
        # 先尝试钉钉任务日志
        log = TaskExecutionLog.query.get(log_id)
        if not log:
            # 再尝试自定义任务日志
            log = CustomTaskLog.query.get(log_id)
        
        if not log:
            flash('日志不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 获取关联的任务
        if isinstance(log, TaskExecutionLog):
            task = ScheduledTask.query.get(log.task_id)
        else:
            task = CustomTask.query.get(log.task_id)
        
        if not task:
            flash('关联的任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限查看此日志')
            return redirect(url_for('scheduled_tasks'))
        
        return render_template('task_log_detail.html', log=log, task=task)

@app.route('/custom_task_logs')
@login_required
def custom_task_logs():
    """自定义任务日志页面"""
    task_id = request.args.get('task_id', type=int)
    task = None
    logs = []
    if task_id:
        # 确保从数据库获取完整的task对象
        task = CustomTask.query.get(task_id)
        if task:
            # 确保created_at字段有值
            if task.created_at is None:
                app.logger.warning(f"任务 {task_id} 的created_at字段为None，设置为当前时间")
                task.created_at = datetime.now()
                db.session.commit()
                
            try:
                # 调用executor_service的API获取日志
                app.logger.info(f"获取自定义任务日志: {task_id}, 调用URL: {EXECUTOR_SERVICE_URL}/api/custom_tasks/{task_id}/logs")
                response = requests.get(f"{EXECUTOR_SERVICE_URL}/api/custom_tasks/{task_id}/logs", timeout=30)
                app.logger.info(f"获取自定义任务日志响应: 状态码={response.status_code}")
                
                if response.status_code == 200:
                    # 从API响应中获取日志数据
                    logs_data = response.json()
                    # 将API返回的日志数据转换为CustomTaskLog对象
                    logs = []
                    for log_data in logs_data:
                        log = CustomTaskLog()
                        log.id = log_data.get('id')
                        log.start_time = datetime.fromisoformat(log_data.get('start_time')) if log_data.get('start_time') else None
                        log.end_time = datetime.fromisoformat(log_data.get('end_time')) if log_data.get('end_time') else None
                        log.duration = log_data.get('duration')
                        log.status = log_data.get('status')
                        log.output = log_data.get('output')
                        log.error_message = log_data.get('error_message')
                        logs.append(log)
                else:
                    app.logger.error(f"获取自定义任务日志失败: {response.text}")
                    flash(f"获取日志失败: {response.text}，尝试从本地数据库获取", 'warning')
                    # 从本地数据库获取日志作为备选方案
                    logs = CustomTaskLog.query.filter_by(task_id=task_id).order_by(
                        CustomTaskLog.start_time.desc()
                    ).limit(50).all()
            except Exception as e:
                app.logger.error(f"获取自定义任务日志异常: {str(e)}")
                flash(f"获取日志异常: {str(e)}，尝试从本地数据库获取", 'warning')
                # 从本地数据库获取日志作为备选方案
                logs = CustomTaskLog.query.filter_by(task_id=task_id).order_by(
                    CustomTaskLog.start_time.desc()
                ).limit(50).all()
    return render_template('custom_task_logs.html', task=task, logs=logs)

@app.route('/package_logs')
@login_required
def package_logs():
    """包日志页面"""
    # 从数据库获取包安装日志
    page = request.args.get('page', 1, type=int)
    per_page = 10
    
    # 查询当前用户的包安装日志，按创建时间倒序排列
    logs_pagination = PackageInstallationLog.query.filter_by(user_id=current_user.id).order_by(
        PackageInstallationLog.created_at.desc()
    ).paginate(page=page, per_page=per_page)
    
    return render_template('package_logs.html', logs=logs_pagination)

@app.route('/package_progress')
@login_required
def package_progress():
    """包进度页面"""
    return render_template('package_progress.html')

@app.route('/package_log/<int:log_id>')
@login_required
def package_log_detail(log_id):
    """获取包安装日志详情"""
    try:
        # 从数据库获取日志详情
        log = PackageInstallationLog.query.get(log_id)
        
        if not log:
            return jsonify({
                'success': False,
                'message': '日志不存在'
            }), 404
        
        # 检查权限
        if log.user_id != current_user.id and not current_user.is_administrator():
            return jsonify({
                'success': False,
                'message': '无权限查看此日志'
            }), 403
        
        # 获取用户名
        user = User.query.get(log.user_id)
        user_name = user.username if user else '未知用户'
        
        # 构建日志详情
        log_data = {
            'id': log.id,
            'package_name': log.package_name,
            'version': log.version,
            'operation': log.operation,
            'status': log.status,
            'output': log.output,
            'error_message': log.error_message,
            'user_id': log.user_id,
            'user_name': user_name,
            'created_at': log.created_at.isoformat() if log.created_at else None,
            'duration': log.duration
        }
        
        return jsonify({
            'success': True,
            'log': log_data
        })
    except Exception as e:
        app.logger.error(f"获取包安装日志详情失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"获取日志详情失败: {str(e)}"
        }), 500

@app.route('/api/get_background_tasks')
@login_required
def get_background_tasks():
    """获取后台任务列表"""
    try:
        from system_scripts.background_task_manager import get_user_running_tasks, cleanup_orphaned_tasks
        from models import BackgroundInstallationTask, db
        
        # 清理孤立任务（数据库中状态为running但内存中不存在）
        cleanup_orphaned_tasks()
        
        # 获取内存中的运行中任务
        running_tasks = get_user_running_tasks(current_user.id)
        
        # 从数据库获取所有任务（包括已完成的）
        db_tasks = BackgroundInstallationTask.query.filter_by(user_id=current_user.id).order_by(BackgroundInstallationTask.created_at.desc()).all()
        
        # 将数据库任务转换为字典格式
        all_tasks = [task.to_dict() for task in db_tasks]
        
        # 用内存中的运行中任务更新数据库任务的状态
        for running_task in running_tasks:
            for task in all_tasks:
                if task['id'] == running_task['id']:
                    # 更新运行中任务的信息
                    task.update(running_task)
                    break
        
        return jsonify({
            'success': True,
            'tasks': all_tasks
        })
    except Exception as e:
        app.logger.error(f"获取后台任务列表失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"获取后台任务列表失败: {str(e)}"
        }), 500

@app.route('/background_task/<int:task_id>')
@login_required
def get_background_task(task_id):
    """获取后台任务详情"""
    try:
        from system_scripts.background_task_manager import get_background_task
        from models import BackgroundInstallationTask, db
        import json
        
        # 从数据库获取任务
        task = BackgroundInstallationTask.query.get(task_id)
        
        if not task:
            return jsonify({
                'success': False,
                'message': '任务不存在'
            }), 404
        
        # 检查权限
        if task.user_id != current_user.id and not current_user.is_administrator():
            return jsonify({
                'success': False,
                'message': '无权访问此任务'
            }), 403
        
        # 获取任务字典
        task_dict = task.to_dict()
        
        # 从内存中获取运行中任务的最新状态
        task_manager = get_background_task(task_id)
        if task_manager and task_manager.is_running:
            # 更新运行中任务的信息
            task_dict.update(task_manager.get_status())
        
        # 解析进度消息
        if task_dict['progress_messages'] and isinstance(task_dict['progress_messages'], str):
            try:
                task_dict['progress_messages'] = json.loads(task_dict['progress_messages'])
            except:
                task_dict['progress_messages'] = []
        
        return jsonify({
            'success': True,
            'task': task_dict
        })
    except Exception as e:
        app.logger.error(f"获取后台任务详情失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"获取后台任务详情失败: {str(e)}"
        }), 500

@app.route('/background_task/<int:task_id>/cancel', methods=['POST'])
@login_required
def cancel_background_task(task_id):
    """取消后台任务"""
    try:
        from system_scripts.background_task_manager import get_background_task
        from models import BackgroundInstallationTask, db
        
        # 从数据库获取任务
        task = BackgroundInstallationTask.query.get(task_id)
        
        if not task:
            return jsonify({
                'success': False,
                'message': '任务不存在'
            }), 404
        
        # 检查权限
        if task.user_id != current_user.id and not current_user.is_administrator():
            return jsonify({
                'success': False,
                'message': '无权操作此任务'
            }), 403
        
        # 获取任务管理器
        task_manager = get_background_task(task_id)
        
        if not task_manager:
            return jsonify({
                'success': False,
                'message': '任务管理器不存在'
            }), 404
        
        # 停止任务
        task_manager.stop()
        
        return jsonify({
            'success': True,
            'message': '任务已取消'
        })
    except Exception as e:
        app.logger.error(f"取消后台任务失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"取消后台任务失败: {str(e)}"
        }), 500

@app.route('/background_task/<int:task_id>/retry', methods=['POST'])
@login_required
def retry_background_task(task_id):
    """重试后台任务"""
    try:
        from system_scripts.background_task_manager import create_background_task, get_background_task
        from models import BackgroundInstallationTask, db
        
        # 从数据库获取任务
        task = BackgroundInstallationTask.query.get(task_id)
        
        if not task:
            return jsonify({
                'success': False,
                'message': '任务不存在'
            }), 404
        
        # 检查权限
        if task.user_id != current_user.id and not current_user.is_administrator():
            return jsonify({
                'success': False,
                'message': '无权操作此任务'
            }), 403
        
        # 检查任务状态
        if task.status != 'failed':
            return jsonify({
                'success': False,
                'message': '只能重试失败的任务'
            }), 400
        
        # 创建新的后台任务
        new_task_id = create_background_task(
            db.session,
            task.user_id,
            task.package_name,
            task.operation,
            task.version,
            task.mirror_source
        )
        
        # 启动新任务
        new_task_manager = get_background_task(new_task_id)
        if new_task_manager:
            new_task_manager.start()
        
        return jsonify({
            'success': True,
            'message': '任务已开始重试',
            'new_task_id': new_task_id
        })
    except Exception as e:
        app.logger.error(f"重试后台任务失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"重试后台任务失败: {str(e)}"
        }), 500

@app.route('/api/cleanup_orphaned_tasks', methods=['POST'])
@login_required
def cleanup_orphaned_tasks_route():
    """清理孤立的后台任务（仅管理员可用）"""
    try:
        # 检查权限
        if not current_user.is_administrator():
            return jsonify({
                'success': False,
                'message': '仅管理员可执行此操作'
            }), 403
        
        # 执行清理
        from system_scripts.background_task_manager import cleanup_orphaned_tasks
        cleanup_orphaned_tasks()
        
        return jsonify({
            'success': True,
            'message': '孤立任务清理完成'
        })
    except Exception as e:
        app.logger.error(f"清理孤立任务失败: {str(e)}")
        return jsonify({
            'success': False,
            'message': f"清理孤立任务失败: {str(e)}"
        }), 500

@app.route('/view_custom_folder')
@app.route('/view_custom_folder/<folder_name>')
@login_required
def view_custom_folder(folder_name=None):
    """查看自定义文件夹页面"""
    # 支持两种方式传递文件夹路径：URL参数和路径参数
    folder_path = folder_name or request.args.get('folder_path', '') or request.args.get('folder_name', '')
    
    if not folder_path:
        flash('文件夹路径不能为空', 'danger')
        return redirect(url_for('extended_scripts'))
    
    # 获取目标用户ID
    target_user_id = request.args.get('user_id', current_user.id)
    
    # 权限检查
    if str(target_user_id) != str(current_user.id) and not current_user.is_administrator():
        flash('无权限查看其他用户的文件夹', 'danger')
        return redirect(url_for('extended_scripts'))
    
    # 获取目标用户
    target_user = User.query.get(target_user_id) or current_user
    
    # 构建完整的文件夹路径
    user_dir = os.path.join('custom_scripts', target_user.username)
    full_folder_path = os.path.join(user_dir, folder_path)
    
    # 检查文件夹是否存在
    if not os.path.exists(full_folder_path):
        flash('文件夹不存在', 'danger')
        return redirect(url_for('extended_scripts'))
    
    # 获取文件夹名称
    folder_name = os.path.basename(folder_path) if folder_path else '根目录'
    
    # 获取文件列表
    files = []
    try:
        for filename in os.listdir(full_folder_path):
            # 过滤隐藏文件（以点开头的文件）
            if filename.startswith('.'):
                continue
                
            file_path = os.path.join(full_folder_path, filename)
            
            # 跳过目录
            if os.path.isdir(file_path):
                continue
            
            # 获取文件信息
            stat = os.stat(file_path)
            file_info = {
                'name': filename,
                'path': file_path,
                'size': stat.st_size,
                'modified_time': datetime.fromtimestamp(stat.st_mtime),
                'is_script': filename.endswith('.py'),
                'is_registered': False,
                'script_id': None,
                'script_name': None
            }
            
            # 如果是Python脚本，检查是否已注册
            if file_info['is_script']:
                # 构建与上传时相同的file_path格式进行查询
                # 上传时保存的是相对于项目根目录的路径，例如：custom_scripts/username/folder/filename.py
                script = CustomScript.query.filter_by(
                    user_id=target_user.id,
                    file_path=file_path
                ).first()
                
                # 如果没找到，尝试使用相对路径查询（兼容旧数据）
                if not script:
                    relative_path = os.path.relpath(file_path, '.')
                    script = CustomScript.query.filter_by(
                        user_id=target_user.id,
                        file_path=relative_path
                    ).first()
                
                # 如果还没找到，尝试使用文件名查询
                if not script:
                    script = CustomScript.query.filter_by(
                        user_id=target_user.id,
                        filename=filename
                    ).first()
                
                if script:
                    file_info['is_registered'] = True
                    file_info['script_id'] = script.id
                    file_info['script_name'] = script.name
            
            files.append(file_info)
        
        # 按文件名排序
        files.sort(key=lambda x: x['name'])
        
    except Exception as e:
        flash(f'读取文件夹内容失败: {str(e)}', 'danger')
        return redirect(url_for('extended_scripts'))
    
    return render_template('view_custom_folder.html', 
                         folder_path=folder_path,
                         folder_name=folder_name,
                         files=files,
                         target_user_id=target_user.id,
                         target_username=target_user.username)

# 此路由已被 reset_password_page 替代
# @app.route('/reset_password')
# def reset_password():
#     """重置密码页面"""
#     return render_template('reset_password.html')

# 添加POST表单处理路由
@app.route('/add_robot', methods=['GET', 'POST'])
@login_required
def add_robot():
    """添加机器人"""
    if request.method == 'POST':
        # 处理表单提交
        try:
            name = request.form.get('name', '').strip()
            tag = request.form.get('tag', '').strip()
            webhook = request.form.get('webhook', '').strip()
            secret = request.form.get('secret', '').strip()
            
            # 验证必填字段
            if not name or not tag or not webhook or not secret:
                flash('所有字段都是必填的', 'error')
                return render_template('add_robot.html')
            
            # 创建新机器人
            robot = DingTalkRobot(
                name=name,
                tag=tag,
                webhook=webhook,
                secret=secret,
                user_id=current_user.id
            )
            
            db.session.add(robot)
            db.session.commit()
            
            flash('机器人添加成功', 'success')
            return redirect(url_for('robot_management'))
            
        except Exception as e:
            db.session.rollback()
            flash(f'添加机器人失败: {str(e)}', 'error')
            return render_template('add_robot.html')
            
    return render_template('add_robot.html')

@app.route('/add_user', methods=['GET', 'POST'])
@login_required
def add_user():
    """添加用户"""
    # 检查权限
    if not current_user.is_administrator():
        flash('只有管理员可以添加用户', 'error')
        return redirect(url_for('dashboard'))

    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        is_admin = request.form.get('is_admin') == 'on'
        can_use_dingtalk = request.form.get('can_use_dingtalk') == 'on'
        can_use_custom_tasks = request.form.get('can_use_custom_tasks') == 'on'
        can_manage_packages = request.form.get('can_manage_packages') == 'on'

        # 验证用户名和密码
        if not username or not password:
            flash('用户名和密码不能为空', 'error')
            return render_template('add_user.html')

        if len(password) < 6:
            flash('密码长度不能少于6位', 'error')
            return render_template('add_user.html')

        # 检查用户名是否已存在
        if User.query.filter_by(username=username).first():
            flash('用户名已存在', 'error')
            return render_template('add_user.html')

        try:
            # 创建新用户
            user = User(
                username=username,
                password=generate_password_hash(password),
                is_admin=is_admin,
                can_use_dingtalk=can_use_dingtalk,
                can_use_custom_tasks=can_use_custom_tasks,
                can_manage_packages=can_manage_packages
            )
            db.session.add(user)
            db.session.commit()
            flash('用户创建成功', 'success')
            return redirect(url_for('user_management'))
        except Exception as e:
            db.session.rollback()
            flash(f'创建用户失败: {str(e)}', 'error')
            return render_template('add_user.html')

    return render_template('add_user.html')

@app.route('/add_task', methods=['GET', 'POST'])
@login_required
def add_task():
    """添加任务"""
    with app.app_context():
        # 根据用户权限获取脚本列表
        if current_user.is_administrator():
            # 管理员可以看到所有脚本
            scripts = Script.query.all()
        else:
            # 普通用户只能看到自己的脚本
            scripts = Script.query.filter_by(user_id=current_user.id).all()
        
        # 根据用户权限获取机器人列表
        if current_user.is_administrator():
            # 管理员可以看到所有机器人
            robots = DingTalkRobot.query.all()
        else:
            # 普通用户只能看到自己的机器人
            robots = DingTalkRobot.query.filter_by(user_id=current_user.id).all()
        
        if request.method == 'POST':
            try:
                # 获取表单数据
                name = request.form.get('name', '').strip()
                cron_expression = request.form.get('cron_expression', '').strip()
                script_path = request.form.get('script_path', '').strip()
                params = request.form.get('params', '{}')
                
                # 处理重试配置字段
                max_retries = 3  # 默认值
                retry_delay = 300  # 默认值
                if 'max_retries' in request.form and request.form['max_retries']:
                    max_retries = int(request.form['max_retries'])
                if 'retry_delay' in request.form and request.form['retry_delay']:
                    retry_delay = int(request.form['retry_delay'])
                
                # 创建新任务
                task = ScheduledTask(
                    name=name,
                    cron_expression=cron_expression,
                    script_path=script_path,
                    params=params,
                    user_id=current_user.id,
                    max_retries=max_retries,
                    retry_delay=retry_delay
                )
                
                # 处理机器人关联
                robot_ids = request.form.get('robot_ids', '').split(',')
                robot_ids = [int(rid) for rid in robot_ids if rid.strip()]
                
                # 处理机器人@人配置
                robot_at_configs = {}
                for key, value in request.form.items():
                    if key.startswith('robot_at_') and value.strip():
                        robot_id = key.replace('robot_at_', '')
                        robot_at_configs[robot_id] = value.strip()
                
                # 保存任务
                db.session.add(task)
                db.session.commit()
                
                # 设置机器人和@人配置（通过多对多表）
                task.set_robots(robot_ids, robot_at_configs)
                
                # 触发调度器重新加载
                reload_scheduler(wait=True, timeout=20)
                
                flash('任务添加成功')
                return redirect(url_for('scheduled_tasks'))
                
            except Exception as e:
                db.session.rollback()
                flash(f'添加任务失败: {str(e)}')
        
        return render_template('add_task.html', scripts=scripts, robots=robots)

# 添加用户依赖检查路由
@app.route('/check_user_dependencies/<int:user_id>')
@login_required
def check_user_dependencies(user_id):
    """检查用户依赖项页面"""
    with app.app_context():
        user = User.query.get(user_id)
        if not user:
            flash('用户不存在')
            return redirect(url_for('user_management'))
        
        # 检查权限
        if not current_user.is_administrator():
            flash('只有管理员可以检查用户依赖项')
            return redirect(url_for('user_management'))
        
        # 获取用户依赖项
        dependencies = user.get_dependencies()
        
        # 获取所有可能的目标用户（不包括当前用户）
        target_users = User.query.filter(User.id != user_id).all()
        
        return render_template('check_user_dependencies.html', user=user, dependencies=dependencies, target_users=target_users)

@app.route('/delete_user/<int:user_id>')
@login_required
def delete_user_page(user_id):
    """删除用户页面"""
    with app.app_context():
        user = User.query.get(user_id)
        if not user:
            flash('用户不存在')
            return redirect(url_for('user_management'))
        
        # 检查权限
        if user.is_admin and current_user.username != 'admin':
            flash('只有超级管理员可以删除管理员用户')
            return redirect(url_for('user_management'))
        
        if user.id == current_user.id:
            flash('不能删除自己')
            return redirect(url_for('user_management'))
        
        # 检查用户是否有依赖项
        if user.has_dependencies():
            # 如果有依赖项，重定向到依赖项检查页面
            return redirect(url_for('check_user_dependencies', user_id=user_id))
        
        # 删除用户
        try:
            db.session.delete(user)
            db.session.commit()
            flash('用户删除成功')
        except Exception as e:
            db.session.rollback()
            flash(f'删除用户失败: {str(e)}')
        
        return redirect(url_for('user_management'))

@app.route('/reset_password/<int:user_id>', methods=['GET', 'POST'])
@login_required
def reset_password_page(user_id):
    """重置密码页面"""
    with app.app_context():
        user = User.query.get(user_id)
        if not user:
            flash('用户不存在')
            return redirect(url_for('user_management'))
        
        # 检查权限
        if user.username == 'admin' and current_user.username != 'admin':
            flash('只有超级管理员可以重置管理员密码')
            return redirect(url_for('user_management'))
        
        if request.method == 'POST':
            # 从表单获取新密码
            new_password = request.form.get('password')
            
            # 更新密码
            try:
                user.password = generate_password_hash(new_password)
                db.session.commit()
                flash(f'密码重置成功')
                return redirect(url_for('user_management'))
            except Exception as e:
                db.session.rollback()
                flash(f'密码重置失败: {str(e)}')
                return redirect(url_for('user_management'))
        
        # GET 请求，显示重置密码页面
        return render_template('reset_password.html', user=user)

@app.route('/delete_script/<int:script_id>')
@login_required
def delete_script_page(script_id):
    """删除脚本页面"""
    with app.app_context():
        script = Script.query.get(script_id)
        if not script:
            flash('脚本不存在')
            return redirect(url_for('script_management'))
        
        # 检查权限
        if not current_user.is_administrator() and script.user_id != current_user.id:
            flash('没有权限删除此脚本')
            return redirect(url_for('script_management'))
        
        # 删除脚本
        try:
            # 先删除关联的脚本版本记录
            ScriptVersion.query.filter_by(script_id=script_id).delete()
            
            # 删除文件
            if os.path.exists(script.path):
                os.remove(script.path)
            
            db.session.delete(script)
            db.session.commit()
            flash('脚本删除成功')
        except Exception as e:
            db.session.rollback()
            flash(f'删除脚本失败: {str(e)}')
        
        return redirect(url_for('script_management'))

@app.route('/download_script/<int:script_id>')
@login_required
def download_script_page(script_id):
    """下载脚本页面"""
    with app.app_context():
        script = Script.query.get(script_id)
        if not script:
            flash('脚本不存在')
            return redirect(url_for('script_management'))
        
        # 检查权限
        if not current_user.is_administrator() and script.user_id != current_user.id:
            flash('没有权限下载此脚本')
            return redirect(url_for('script_management'))
        
        # 检查文件是否存在
        if not os.path.exists(script.path):
            flash('脚本文件不存在')
            return redirect(url_for('script_management'))
        
        # 返回文件下载
        from flask import send_file
        return send_file(script.path, as_attachment=True, download_name=f"{script.name}.py")

@app.route('/delete_robot/<int:robot_id>')
@login_required
def delete_robot_page(robot_id):
    """删除机器人页面"""
    with app.app_context():
        robot = DingTalkRobot.query.get(robot_id)
        if not robot:
            flash('机器人不存在')
            return redirect(url_for('robot_management'))
        
        # 检查权限
        if not current_user.is_administrator() and robot.user_id != current_user.id:
            flash('没有权限删除此机器人')
            return redirect(url_for('robot_management'))
        
        # 删除机器人
        try:
            db.session.delete(robot)
            db.session.commit()
            flash('机器人删除成功')
        except Exception as e:
            db.session.rollback()
            flash(f'删除机器人失败: {str(e)}')
        
        return redirect(url_for('robot_management'))

@app.route('/run_task/<int:task_id>')
@login_required
def run_task_page(task_id):
    """运行任务页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限执行此任务')
            return redirect(url_for('scheduled_tasks'))
        
        # 执行任务
        try:
            if isinstance(task, ScheduledTask):
                # 钉钉任务
                response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/tasks/{task_id}/run", timeout=30)
            else:
                # 自定义任务
                response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/run_custom_task/{task_id}", timeout=30)
            
            if response.status_code == 200:
                flash('任务已开始执行')
            else:
                flash('任务执行失败')
        except Exception as e:
            flash(f'任务执行失败: {str(e)}')
        
        return redirect(url_for('scheduled_tasks'))

@app.route('/stop_task/<int:task_id>')
@login_required
def stop_task_page(task_id):
    """停止任务页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限停止此任务')
            return redirect(url_for('scheduled_tasks'))
        
        # 停止任务（这里只是更新状态）
        try:
            task.status = 'pending'  # 现在CustomTask也有status字段了
            db.session.commit()
            flash('任务已停止')
        except Exception as e:
            flash(f'停止任务失败: {str(e)}')
        
        return redirect(url_for('scheduled_tasks'))

@app.route('/toggle_task/<int:task_id>')
@login_required
def toggle_task_page(task_id):
    """切换任务状态页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限操作此任务')
            return redirect(url_for('scheduled_tasks'))
        
        # 切换任务状态
        try:
            task.is_active = not task.is_active
            db.session.commit()
            
            # 触发调度器重新加载
            reload_scheduler(wait=True, timeout=20)
            
            flash(f'任务已{"启用" if task.is_active else "禁用"}')
        except Exception as e:
            flash(f'操作任务失败: {str(e)}')
        
        return redirect(url_for('scheduled_tasks'))

@app.route('/task_logs/<int:task_id>')
@login_required
def task_logs_page(task_id):
    """任务日志页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            flash('没有权限查看此任务日志')
            return redirect(url_for('scheduled_tasks'))
        
        # 获取任务日志
        if isinstance(task, ScheduledTask):
            # 钉钉任务日志
            logs = TaskExecutionLog.query.filter_by(task_id=task_id).order_by(
                TaskExecutionLog.start_time.desc()
            ).limit(50).all()
        else:
            # 自定义任务日志
            logs = CustomTaskLog.query.filter_by(task_id=task_id).order_by(
                CustomTaskLog.start_time.desc()
            ).limit(50).all()
        
        return render_template('task_logs.html', task=task, logs=logs)

@app.route('/delete_task/<int:task_id>', methods=['GET', 'POST'])
@login_required
def delete_task_page(task_id):
    """删除任务页面"""
    with app.app_context():
        # 先尝试钉钉任务
        task = ScheduledTask.query.get(task_id)
        if not task:
            # 再尝试自定义任务
            task = CustomTask.query.get(task_id)
        
        if not task:
            # 检查是否是AJAX请求
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return jsonify({'status': 'error', 'message': '任务不存在'}), 404
            flash('任务不存在')
            return redirect(url_for('scheduled_tasks'))
        
        # 检查权限
        if not current_user.is_administrator() and task.user_id != current_user.id:
            # 检查是否是AJAX请求
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return jsonify({'status': 'error', 'message': '没有权限删除此任务'}), 403
            flash('没有权限删除此任务')
            return redirect(url_for('scheduled_tasks'))
        
        # 删除任务
        try:
            # 记录日志
            app.logger.info(f'用户 {current_user.username} 正在删除任务 {task.id}: {task.name}')
            
            # 删除关联的日志
            if isinstance(task, ScheduledTask):
                log_count = TaskExecutionLog.query.filter_by(task_id=task_id).count()
                TaskExecutionLog.query.filter_by(task_id=task_id).delete()
                app.logger.info(f'已删除 {log_count} 条关联的执行日志')
                
                # 删除任务与机器人的关联记录
                assoc_count = TaskRobotAssociation.query.filter_by(task_id=task_id).count()
                TaskRobotAssociation.query.filter_by(task_id=task_id).delete()
                app.logger.info(f'已删除 {assoc_count} 条任务与机器人的关联记录')
            elif isinstance(task, CustomTask):
                log_count = CustomTaskLog.query.filter_by(task_id=task_id).count()
                CustomTaskLog.query.filter_by(task_id=task_id).delete()
                app.logger.info(f'已删除 {log_count} 条关联的执行日志')
            
            # 记录任务名称以便在删除后显示
            task_name = task.name
            
            # 删除任务
            db.session.delete(task)
            db.session.commit()
            app.logger.info(f'任务 {task_id}: {task_name} 已成功删除')
            
            # 触发调度器重新加载
            try:
                reload_response = requests.post(f"{SCHEDULER_SERVICE_URL}/api/reload", json={'wait': True}, timeout=5)
                app.logger.info(f'调度器重新加载响应: {reload_response.status_code}')
            except Exception as reload_error:
                app.logger.error(f'触发调度器重新加载失败: {str(reload_error)}')
            
            # 检查是否是AJAX请求
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return jsonify({'status': 'success', 'message': f'任务 {task_name} 已成功删除'})
            
            flash('任务删除成功')
        except Exception as e:
            app.logger.error(f'删除任务 {task_id} 失败: {str(e)}')
            # 检查是否是AJAX请求
            if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
                return jsonify({'status': 'error', 'message': f'删除任务失败: {str(e)}'}), 500
            flash(f'删除任务失败: {str(e)}')
        
        return redirect(url_for('scheduled_tasks'))

@app.route('/system_monitor')
def system_monitor_page():
    """系统监控页面"""
    return render_template('system_monitor.html')

@app.route('/api/health')
def web_health_check():
    """健康检查端点"""
    return jsonify({"status": "ok"}), 200

@app.route('/api/monitor/status')
def get_monitor_status():
    """获取系统资源监控状态"""
    try:
        monitor = ResourceMonitor()
        status = monitor.check_system_resources()
        return jsonify(status)
    except Exception as e:
        app.logger.error(f"获取监控状态失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/monitor/history')
def get_alert_history():
    """获取告警历史"""
    try:
        alert_manager = AlertManager()
        history = alert_manager.get_alert_history()
        return jsonify(history)
    except Exception as e:
        app.logger.error(f"获取告警历史失败: {str(e)}")
        # 返回一个空的告警历史列表，避免前端解析错误
        return jsonify([])

# 管理员权限检查装饰器
def admin_required(f):
    """确保只有管理员用户可以访问该路由"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or not current_user.is_administrator():
            flash('只有管理员可以访问此功能', 'warning')
            return redirect(url_for('system_monitor_page'))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/api/monitor/config', methods=['GET', 'POST'])
def monitor_config():
    """获取或更新监控配置"""
    try:
        if request.method == 'GET':
            monitor = ResourceMonitor()
            return jsonify(monitor.get_config())
        else:
            config = request.get_json()
            monitor = ResourceMonitor()
            monitor.update_config(config)
            return jsonify({'message': '监控配置已更新'})
    except Exception as e:
        app.logger.error(f"操作监控配置失败: {str(e)}")
        if request.method == 'GET':
            # 返回默认监控配置，避免前端解析错误
            return jsonify({
                'cpu_threshold': {'warning': 70, 'critical': 90},
                'memory_threshold': {'warning': 70, 'critical': 90},
                'disk_threshold': {'warning': 70, 'critical': 90}
            })
        else:
            return jsonify({'message': '监控配置更新失败，请稍后重试'})

@app.route('/api/monitor/alert/config', methods=['GET', 'POST'])
def alert_config():
    """获取或更新告警配置"""
    try:
        alert_manager = AlertManager()
        if request.method == 'GET':
            return jsonify(alert_manager.get_config())
        else:
            config = request.get_json()
            alert_manager.update_config(config)
            return jsonify({'message': '告警配置已更新'})
    except Exception as e:
        app.logger.error(f"操作告警配置失败: {str(e)}")
        if request.method == 'GET':
            # 返回默认告警配置，避免前端解析错误
            return jsonify({
                'enabled': False,
                'email': {'enabled': False, 'recipients': []},
                'dingtalk': {'enabled': False, 'webhook': ''}
            })
        else:
            return jsonify({'message': '告警配置更新失败，请稍后重试'})

@app.route('/api/monitor/metrics/system')
def get_system_metrics():
    """获取系统资源使用指标"""
    try:
        monitor = ResourceMonitor()
        metrics = monitor.get_system_metrics()
        return jsonify(metrics)
    except Exception as e:
        app.logger.error(f"获取系统指标失败: {str(e)}")
        # 返回一个与预期格式一致的默认数据结构，避免前端解析错误
        return jsonify({
            'cpu': {'percent': 0, 'status': 'normal'},
            'memory': {'percent': 0, 'status': 'normal'},
            'disk': {'percent': 0, 'status': 'normal'}
        })

@app.route('/api/monitor/metrics/process')
@login_required
def get_process_metrics():
    """获取指定进程的资源使用指标"""
    try:
        process_name = request.args.get('name')
        if not process_name:
            return jsonify({'error': '缺少进程名称参数'}), 400
            
        monitor = ResourceMonitor()
        metrics = monitor.get_process_metrics(process_name)
        return jsonify(metrics)
    except Exception as e:
        app.logger.error(f"获取进程指标失败: {str(e)}")
        return jsonify({'error': str(e)}), 500

@app.route('/api/monitor/services')
def get_services_status_proxy():
    """获取服务状态"""
    try:
        # 尝试从监控服务获取状态
        try:
            response = requests.get(f"{MONITOR_SERVICE_URL}/api/monitor/services", timeout=3)
            if response.status_code == 200:
                return jsonify(response.json())
            raise Exception(f"监控服务返回状态码: {response.status_code}")
        except Exception as e:
            app.logger.warning(f"从监控服务获取状态失败，将直接检查服务状态: {str(e)}")
            
        # 如果从监控服务获取失败，直接检查各服务状态
        # 计算Web服务的在线时长
        web_uptime = "当前会话"
        try:
            # 尝试获取当前进程的启动时间
            current_process = psutil.Process()
            start_time = datetime.fromtimestamp(current_process.create_time())
            uptime = datetime.now() - start_time
            # 格式化为天、小时、分钟
            days = uptime.days
            hours, remainder = divmod(uptime.seconds, 3600)
            minutes, _ = divmod(remainder, 60)
            web_uptime = f"{days}天 {hours}小时 {minutes}分钟"
        except Exception as e:
            app.logger.warning(f"计算Web服务在线时长失败: {str(e)}")
            
        services = [
            {"name": "Web服务", "url": "http://localhost:8080", "status": True, "uptime": web_uptime},  # Web服务肯定在线
            {"name": "调度器服务", "url": SCHEDULER_SERVICE_URL, "status": False, "uptime": ""},
            {"name": "执行器服务", "url": EXECUTOR_SERVICE_URL, "status": False, "uptime": ""},
            {"name": "监控服务", "url": MONITOR_SERVICE_URL, "status": False, "uptime": ""}
        ]
        
        # 检查各个服务的状态（除了Web服务，它已经确认在线）
        for i in range(1, len(services)):
            service = services[i]
            try:
                # 尝试连接服务的健康检查端点
                response = requests.get(f"{service['url']}/api/health", timeout=1)
                service["status"] = response.status_code == 200
                if service["status"]:
                    # 尝试获取进程启动时间并计算在线时长
                    try:
                        port = int(service["url"].split(":")[-1])
                        found = False
                        for proc in psutil.process_iter(['pid', 'name', 'create_time']):
                            try:
                                connections = proc.connections()
                                for conn in connections:
                                    if hasattr(conn, 'laddr') and hasattr(conn.laddr, 'port') and conn.laddr.port == port:
                                        # 计算运行时长
                                        start_time = datetime.fromtimestamp(proc.info['create_time'])
                                        uptime = datetime.now() - start_time
                                        # 格式化为天、小时、分钟
                                        days = uptime.days
                                        hours, remainder = divmod(uptime.seconds, 3600)
                                        minutes, _ = divmod(remainder, 60)
                                        service["uptime"] = f"{days}天 {hours}小时 {minutes}分钟"
                                        found = True
                                        break
                                if found:
                                    break
                            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                                pass
                        # 如果没有找到进程，设置默认在线时长为"在线"
                        if not service.get("uptime"):
                            service["uptime"] = "在线"
                    except Exception as e:
                        app.logger.warning(f"计算服务 {service['name']} 在线时长失败: {str(e)}")
                        service["uptime"] = "在线"
                app.logger.info(f"服务 {service['name']} 状态检查: {service['status']}")
            except Exception as e:
                app.logger.warning(f"检查服务 {service['name']} 状态失败: {str(e)}")
                service["status"] = False
        
        return jsonify(services)
    except Exception as e:
        app.logger.error(f"获取服务状态时发生异常: {str(e)}", exc_info=True)
        # 发生异常时，至少确保返回Web服务在线
        # 尝试获取Web服务的在线时长
        web_uptime = "当前会话"
        try:
            # 尝试获取当前进程的启动时间
            current_process = psutil.Process()
            start_time = datetime.fromtimestamp(current_process.create_time())
            uptime = datetime.now() - start_time
            # 格式化为天、小时、分钟
            days = uptime.days
            hours, remainder = divmod(uptime.seconds, 3600)
            minutes, _ = divmod(remainder, 60)
            web_uptime = f"{days}天 {hours}小时 {minutes}分钟"
        except Exception as e:
            app.logger.warning(f"计算Web服务在线时长失败: {str(e)}")
            
        return jsonify([
            {"name": "Web服务", "status": True, "uptime": web_uptime},
            {"name": "调度器服务", "status": False, "uptime": ""},
            {"name": "执行器服务", "status": False, "uptime": ""},
            {"name": "监控服务", "status": False, "uptime": ""}
        ])

@app.route('/custom_parameters')
@login_required
def custom_parameters_page():
    """自定义参数管理页面"""
    # 获取当前登录用户的用户名
    username = current_user.username
    parameters = parameters_manager.get_all_parameters(username)
    return render_template('custom_parameters.html', parameters=parameters, username=username)

@app.route('/api/custom_parameters', methods=['POST'])
@login_required
def add_custom_parameter():
    """添加自定义参数API"""
    data = request.json
    key = data.get('key')
    value = data.get('value')
    
    if not key or not value:
        return jsonify({'success': False, 'message': '参数名和参数值不能为空'})
    
    # 获取当前登录用户的用户名
    username = current_user.username
    success, message = parameters_manager.add_parameter(key, value, username)
    return jsonify({'success': success, 'message': message})

@app.route('/api/custom_parameters/<key>', methods=['DELETE'])
@login_required
def delete_custom_parameter(key):
    """删除自定义参数API"""
    # 获取当前登录用户的用户名
    username = current_user.username
    success, message = parameters_manager.delete_parameter(key, username)
    return jsonify({'success': success, 'message': message})

@app.route('/api/view_file_content')
@login_required
def view_file_content():
    """查看文件内容API"""
    try:
        file_path = request.args.get('path')
        if not file_path:
            return jsonify({'error': '缺少文件路径'}), 400
            
        # 确保file_path是字符串类型
        if not isinstance(file_path, str):
            app.logger.warning(f"view_file_content接口file_path参数类型异常: {type(file_path)}")
            try:
                file_path = str(file_path)
            except Exception as e:
                app.logger.error(f"view_file_content接口file_path参数转换为字符串失败: {str(e)}")
                return jsonify({'error': '文件路径格式错误'}), 400
        
        # 安全检查：确保文件路径在允许的目录内
        if not os.path.exists(file_path):
            app.logger.warning(f"view_file_content接口文件不存在: {file_path}")
            return jsonify({'error': '文件不存在'}), 404
        
        # 检查文件大小，避免读取过大的文件
        file_size = os.path.getsize(file_path)
        if file_size > 5 * 1024 * 1024:  # 5MB限制
            return jsonify({'error': '文件过大，无法预览（限制5MB）'}), 400
        
        # 尝试以不同编码读取文本文件
        encodings = ['utf-8', 'gbk', 'latin-1', 'cp1252', 'iso-8859-1']
        content = None
        
        for encoding in encodings:
            try:
                with open(file_path, 'r', encoding=encoding) as f:
                    content = f.read()
                break
            except UnicodeDecodeError:
                continue
        
        if content is None:
            app.logger.warning(f"view_file_content接口无法解码文件: {file_path}")
            return jsonify({'error': '文件编码不支持，无法预览'}), 400
        
        return jsonify({'content': content})
    except PermissionError:
        app.logger.warning(f"view_file_content接口无权限读取文件: {file_path}")
        return jsonify({'error': '无权限读取文件'}), 403
    except IsADirectoryError:
        app.logger.warning(f"view_file_content接口尝试读取目录: {file_path}")
        return jsonify({'error': '无法预览目录'}), 400
    except FileNotFoundError:
        app.logger.warning(f"view_file_content接口文件不存在: {file_path}")
        return jsonify({'error': '文件不存在'}), 404
    except Exception as e:
        app.logger.error(f"查看文件内容失败: {str(e)}")
        return jsonify({'error': f'读取文件失败: {str(e)}'}), 500

def init_monitor_service():
    """初始化监控服务"""
    try:
        # 检查是否在微服务模式下运行
        is_microservice = os.environ.get('SCHEDULER_SERVICE_URL') or os.environ.get('EXECUTOR_SERVICE_URL')
        
        if not is_microservice:
            # 单体模式下，直接初始化监控服务
            try:
                # 动态导入，避免循环导入问题
                from services.monitor_service import MonitorService
                monitor_service = MonitorService()
                
                # 初始化监控服务，但不启动Flask应用
                # 避免在这里调用monitor_service.start()，因为它会在5003端口启动Flask应用
                # 而是只初始化调度器
                monitor_service.init_scheduler()
                
                app.logger.info("监控服务已初始化（单体模式）")
            except ImportError as e:
                app.logger.warning(f"无法导入监控服务模块: {str(e)}")
            except Exception as e:
                app.logger.error(f"初始化监控服务失败: {str(e)}")
        else:
            # 微服务模式下，不初始化本地监控服务
            app.logger.info("微服务模式下运行，不初始化本地监控服务")
            # 确保MONITOR_SERVICE_URL环境变量已设置
            if not os.environ.get('MONITOR_SERVICE_URL'):
                app.logger.warning("微服务模式下未设置MONITOR_SERVICE_URL环境变量，将使用默认值http://localhost:5003")
    except Exception as e:
        app.logger.error(f"初始化监控服务失败: {str(e)}")

if __name__ == '__main__':
    # 从命令行参数获取host
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--host', default='0.0.0.0', help='服务监听地址')
    args = parser.parse_args()
    
    with app.app_context():
        db.create_all()
        init_monitor_service()
        
        # 清理孤立的后台任务
        from system_scripts.background_task_manager import cleanup_orphaned_tasks
        cleanup_orphaned_tasks()
        app.logger.info("已清理孤立的后台任务")
    
    # 使用配置的host和port启动服务
    app.run(host=args.host, port=app.config['PORT'], debug=False)  # 关闭调试模式，避免安全问题